1use crate::action::Action;
129use crate::sandbox::Sandbox;
130use crate::scan::ResolvedPackage;
131use anyhow::{Context, Result, anyhow, bail};
132use mlua::{Lua, RegistryKey, Result as LuaResult, Table, Value};
133use pkgsrc::PkgPath;
134use std::collections::HashMap;
135use std::path::{Path, PathBuf};
136use std::sync::{Arc, Mutex};
137
138#[derive(Clone, Debug)]
144pub struct PkgsrcEnv {
145 pub packages: PathBuf,
147 pub pkgtools: PathBuf,
149 pub prefix: PathBuf,
151 pub pkg_dbdir: PathBuf,
153 pub pkg_refcount_dbdir: PathBuf,
155 pub cachevars: HashMap<String, String>,
157}
158
159impl PkgsrcEnv {
160 pub fn fetch(config: &Config, sandbox: &Sandbox) -> Result<Self> {
165 const REQUIRED_VARS: &[&str] = &[
166 "PACKAGES",
167 "PKG_DBDIR",
168 "PKG_REFCOUNT_DBDIR",
169 "PKG_TOOLS_BIN",
170 "PREFIX",
171 ];
172
173 let user_cachevars = config.cachevars();
174 let mut all_varnames: Vec<&str> = REQUIRED_VARS.to_vec();
175 for v in user_cachevars {
176 all_varnames.push(v.as_str());
177 }
178
179 let varnames_arg = all_varnames.join(" ");
180 let script = format!(
181 "cd {}/pkgtools/pkg_install && {} show-vars VARNAMES=\"{}\"\n",
182 config.pkgsrc().display(),
183 config.make().display(),
184 varnames_arg
185 );
186
187 let child = sandbox.execute_script(0, &script, vec![])?;
188 let output = child
189 .wait_with_output()
190 .context("Failed to execute bmake show-vars")?;
191
192 if !output.status.success() {
193 let stderr = String::from_utf8_lossy(&output.stderr);
194 bail!("Failed to query pkgsrc variables: {}", stderr.trim());
195 }
196
197 let stdout = String::from_utf8_lossy(&output.stdout);
198 let lines: Vec<&str> = stdout.lines().collect();
199
200 if lines.len() != all_varnames.len() {
201 bail!(
202 "Expected {} variables from pkgsrc, got {}",
203 all_varnames.len(),
204 lines.len()
205 );
206 }
207
208 let mut values: HashMap<&str, &str> = HashMap::new();
209 for (varname, value) in all_varnames.iter().zip(&lines) {
210 values.insert(varname, value);
211 }
212
213 for varname in REQUIRED_VARS {
214 if values.get(varname).is_none_or(|v| v.is_empty()) {
215 bail!("pkgsrc returned empty value for {}", varname);
216 }
217 }
218
219 let mut cachevars: HashMap<String, String> = HashMap::new();
220 for varname in user_cachevars {
221 if let Some(value) = values.get(varname.as_str()) {
222 if !value.is_empty() {
223 cachevars.insert(varname.clone(), (*value).to_string());
224 }
225 }
226 }
227
228 Ok(PkgsrcEnv {
229 packages: PathBuf::from(values["PACKAGES"]),
230 pkgtools: PathBuf::from(values["PKG_TOOLS_BIN"]),
231 prefix: PathBuf::from(values["PREFIX"]),
232 pkg_dbdir: PathBuf::from(values["PKG_DBDIR"]),
233 pkg_refcount_dbdir: PathBuf::from(values["PKG_REFCOUNT_DBDIR"]),
234 cachevars,
235 })
236 }
237}
238
239#[derive(Clone)]
241pub struct LuaEnv {
242 lua: Arc<Mutex<Lua>>,
243 env_key: Option<Arc<RegistryKey>>,
244}
245
246impl std::fmt::Debug for LuaEnv {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("LuaEnv")
249 .field("has_env", &self.env_key.is_some())
250 .finish()
251 }
252}
253
254impl Default for LuaEnv {
255 fn default() -> Self {
256 Self { lua: Arc::new(Mutex::new(Lua::new())), env_key: None }
257 }
258}
259
260impl LuaEnv {
261 pub fn get_env(
264 &self,
265 pkg: &ResolvedPackage,
266 ) -> Result<HashMap<String, String>, String> {
267 let Some(env_key) = &self.env_key else {
268 return Ok(HashMap::new());
269 };
270
271 let lua =
272 self.lua.lock().map_err(|e| format!("Lua lock error: {}", e))?;
273
274 let env_value: Value = lua
276 .registry_value(env_key)
277 .map_err(|e| format!("Failed to get env from registry: {}", e))?;
278
279 let idx = &pkg.index;
280
281 let result_table: Table = match env_value {
282 Value::Function(func) => {
284 let pkg_table = lua
285 .create_table()
286 .map_err(|e| format!("Failed to create table: {}", e))?;
287
288 pkg_table
290 .set("pkgname", idx.pkgname.to_string())
291 .map_err(|e| format!("Failed to set pkgname: {}", e))?;
292 pkg_table
293 .set("pkgpath", pkg.pkgpath.as_path().display().to_string())
294 .map_err(|e| format!("Failed to set pkgpath: {}", e))?;
295 pkg_table
296 .set(
297 "all_depends",
298 idx.all_depends
299 .as_ref()
300 .map(|deps| {
301 deps.iter()
302 .map(|d| {
303 d.pkgpath()
304 .as_path()
305 .display()
306 .to_string()
307 })
308 .collect::<Vec<_>>()
309 .join(" ")
310 })
311 .unwrap_or_default(),
312 )
313 .map_err(|e| format!("Failed to set all_depends: {}", e))?;
314 pkg_table
315 .set(
316 "pkg_skip_reason",
317 idx.pkg_skip_reason.clone().unwrap_or_default(),
318 )
319 .map_err(|e| {
320 format!("Failed to set pkg_skip_reason: {}", e)
321 })?;
322 pkg_table
323 .set(
324 "pkg_fail_reason",
325 idx.pkg_fail_reason.clone().unwrap_or_default(),
326 )
327 .map_err(|e| {
328 format!("Failed to set pkg_fail_reason: {}", e)
329 })?;
330 pkg_table
331 .set(
332 "no_bin_on_ftp",
333 idx.no_bin_on_ftp.clone().unwrap_or_default(),
334 )
335 .map_err(|e| {
336 format!("Failed to set no_bin_on_ftp: {}", e)
337 })?;
338 pkg_table
339 .set(
340 "restricted",
341 idx.restricted.clone().unwrap_or_default(),
342 )
343 .map_err(|e| format!("Failed to set restricted: {}", e))?;
344 pkg_table
345 .set(
346 "categories",
347 idx.categories.clone().unwrap_or_default(),
348 )
349 .map_err(|e| format!("Failed to set categories: {}", e))?;
350 pkg_table
351 .set(
352 "maintainer",
353 idx.maintainer.clone().unwrap_or_default(),
354 )
355 .map_err(|e| format!("Failed to set maintainer: {}", e))?;
356 pkg_table
357 .set(
358 "use_destdir",
359 idx.use_destdir.clone().unwrap_or_default(),
360 )
361 .map_err(|e| format!("Failed to set use_destdir: {}", e))?;
362 pkg_table
363 .set(
364 "bootstrap_pkg",
365 idx.bootstrap_pkg.clone().unwrap_or_default(),
366 )
367 .map_err(|e| {
368 format!("Failed to set bootstrap_pkg: {}", e)
369 })?;
370 pkg_table
371 .set(
372 "usergroup_phase",
373 idx.usergroup_phase.clone().unwrap_or_default(),
374 )
375 .map_err(|e| {
376 format!("Failed to set usergroup_phase: {}", e)
377 })?;
378 pkg_table
379 .set(
380 "scan_depends",
381 idx.scan_depends
382 .as_ref()
383 .map(|deps| {
384 deps.iter()
385 .map(|p| p.display().to_string())
386 .collect::<Vec<_>>()
387 .join(" ")
388 })
389 .unwrap_or_default(),
390 )
391 .map_err(|e| {
392 format!("Failed to set scan_depends: {}", e)
393 })?;
394 pkg_table
395 .set(
396 "pbulk_weight",
397 idx.pbulk_weight.clone().unwrap_or_default(),
398 )
399 .map_err(|e| {
400 format!("Failed to set pbulk_weight: {}", e)
401 })?;
402 pkg_table
403 .set(
404 "multi_version",
405 idx.multi_version
406 .as_ref()
407 .map(|v| v.join(" "))
408 .unwrap_or_default(),
409 )
410 .map_err(|e| {
411 format!("Failed to set multi_version: {}", e)
412 })?;
413 pkg_table
414 .set(
415 "depends",
416 pkg.depends()
417 .iter()
418 .map(|d| d.to_string())
419 .collect::<Vec<_>>()
420 .join(" "),
421 )
422 .map_err(|e| format!("Failed to set depends: {}", e))?;
423
424 func.call(pkg_table).map_err(|e| {
425 format!("Failed to call env function: {}", e)
426 })?
427 }
428 Value::Table(t) => t,
430 Value::Nil => return Ok(HashMap::new()),
431 _ => return Err("env must be a function or table".to_string()),
432 };
433
434 let mut env = HashMap::new();
436 for pair in result_table.pairs::<String, String>() {
437 let (k, v) = pair
438 .map_err(|e| format!("Failed to iterate env table: {}", e))?;
439 env.insert(k, v);
440 }
441
442 Ok(env)
443 }
444}
445
446#[derive(Clone, Debug, Default)]
448pub struct Config {
449 file: ConfigFile,
450 log_level: String,
451 lua_env: LuaEnv,
452}
453
454#[derive(Clone, Debug, Default)]
456pub struct ConfigFile {
457 pub options: Option<Options>,
459 pub pkgsrc: Pkgsrc,
461 pub scripts: HashMap<String, PathBuf>,
463 pub sandboxes: Option<Sandboxes>,
465}
466
467#[derive(Clone, Debug, Default)]
474pub struct Options {
475 pub build_threads: Option<usize>,
477 pub scan_threads: Option<usize>,
479 pub strict_scan: Option<bool>,
481 pub log_level: Option<String>,
483}
484
485#[derive(Clone, Debug, Default)]
501pub struct Pkgsrc {
502 pub basedir: PathBuf,
504 pub bootstrap: Option<PathBuf>,
506 pub build_user: Option<String>,
508 pub logdir: PathBuf,
510 pub make: PathBuf,
512 pub pkgpaths: Option<Vec<PkgPath>>,
514 pub save_wrkdir_patterns: Vec<String>,
516 pub cachevars: Vec<String>,
518 pub tar: Option<PathBuf>,
520}
521
522#[derive(Clone, Debug, Default)]
539pub struct Sandboxes {
540 pub basedir: PathBuf,
545 pub actions: Vec<Action>,
549}
550
551impl Config {
552 pub fn load(config_path: Option<&Path>) -> Result<Config> {
563 let filename = if let Some(path) = config_path {
567 path.to_path_buf()
568 } else {
569 std::env::current_dir()
570 .context("Unable to determine current directory")?
571 .join("config.lua")
572 };
573
574 if !filename.exists() {
576 anyhow::bail!(
577 "Configuration file {} does not exist",
578 filename.display()
579 );
580 }
581
582 let (mut file, lua_env) =
586 load_lua(&filename).map_err(|e| anyhow!(e)).with_context(|| {
587 format!(
588 "Unable to parse Lua configuration file {}",
589 filename.display()
590 )
591 })?;
592
593 let base_dir = filename.parent().unwrap_or_else(|| Path::new("."));
598 let mut newscripts: HashMap<String, PathBuf> = HashMap::new();
599 for (k, v) in &file.scripts {
600 let fullpath =
601 if v.is_relative() { base_dir.join(v) } else { v.clone() };
602 newscripts.insert(k.clone(), fullpath);
603 }
604 file.scripts = newscripts;
605
606 if let Some(ref bootstrap) = file.pkgsrc.bootstrap {
610 if !bootstrap.exists() {
611 anyhow::bail!(
612 "pkgsrc.bootstrap file {} does not exist",
613 bootstrap.display()
614 );
615 }
616 }
617
618 let log_level = if let Some(opts) = &file.options {
622 opts.log_level.clone().unwrap_or_else(|| "info".to_string())
623 } else {
624 "info".to_string()
625 };
626
627 Ok(Config { file, log_level, lua_env })
628 }
629
630 pub fn build_threads(&self) -> usize {
631 if let Some(opts) = &self.file.options {
632 opts.build_threads.unwrap_or(1)
633 } else {
634 1
635 }
636 }
637
638 pub fn scan_threads(&self) -> usize {
639 if let Some(opts) = &self.file.options {
640 opts.scan_threads.unwrap_or(1)
641 } else {
642 1
643 }
644 }
645
646 pub fn strict_scan(&self) -> bool {
647 if let Some(opts) = &self.file.options {
648 opts.strict_scan.unwrap_or(false)
649 } else {
650 false
651 }
652 }
653
654 pub fn script(&self, key: &str) -> Option<&PathBuf> {
655 self.file.scripts.get(key)
656 }
657
658 pub fn make(&self) -> &PathBuf {
659 &self.file.pkgsrc.make
660 }
661
662 pub fn pkgpaths(&self) -> &Option<Vec<PkgPath>> {
663 &self.file.pkgsrc.pkgpaths
664 }
665
666 pub fn pkgsrc(&self) -> &PathBuf {
667 &self.file.pkgsrc.basedir
668 }
669
670 pub fn sandboxes(&self) -> &Option<Sandboxes> {
671 &self.file.sandboxes
672 }
673
674 pub fn log_level(&self) -> &str {
675 &self.log_level
676 }
677
678 pub fn logdir(&self) -> &PathBuf {
679 &self.file.pkgsrc.logdir
680 }
681
682 pub fn save_wrkdir_patterns(&self) -> &[String] {
683 self.file.pkgsrc.save_wrkdir_patterns.as_slice()
684 }
685
686 pub fn tar(&self) -> Option<&PathBuf> {
687 self.file.pkgsrc.tar.as_ref()
688 }
689
690 pub fn build_user(&self) -> Option<&str> {
691 self.file.pkgsrc.build_user.as_deref()
692 }
693
694 pub fn bootstrap(&self) -> Option<&PathBuf> {
695 self.file.pkgsrc.bootstrap.as_ref()
696 }
697
698 pub fn cachevars(&self) -> &[String] {
700 self.file.pkgsrc.cachevars.as_slice()
701 }
702
703 pub fn get_pkg_env(
705 &self,
706 pkg: &ResolvedPackage,
707 ) -> Result<std::collections::HashMap<String, String>, String> {
708 self.lua_env.get_env(pkg)
709 }
710
711 pub fn script_env(
721 &self,
722 pkgsrc_env: Option<&PkgsrcEnv>,
723 ) -> Vec<(String, String)> {
724 let mut envs = vec![
725 ("bob_logdir".to_string(), format!("{}", self.logdir().display())),
726 ("bob_make".to_string(), format!("{}", self.make().display())),
727 ("bob_pkgsrc".to_string(), format!("{}", self.pkgsrc().display())),
728 ];
729 if let Some(env) = pkgsrc_env {
730 envs.push((
731 "bob_packages".to_string(),
732 env.packages.display().to_string(),
733 ));
734 envs.push((
735 "bob_pkgtools".to_string(),
736 env.pkgtools.display().to_string(),
737 ));
738 envs.push((
739 "bob_prefix".to_string(),
740 env.prefix.display().to_string(),
741 ));
742 envs.push((
743 "bob_pkg_dbdir".to_string(),
744 env.pkg_dbdir.display().to_string(),
745 ));
746 envs.push((
747 "bob_pkg_refcount_dbdir".to_string(),
748 env.pkg_refcount_dbdir.display().to_string(),
749 ));
750 for (key, value) in &env.cachevars {
751 envs.push((key.clone(), value.clone()));
752 }
753 }
754 let tar_value = self
755 .tar()
756 .map(|t| t.display().to_string())
757 .unwrap_or_else(|| "tar".to_string());
758 envs.push(("bob_tar".to_string(), tar_value));
759 if let Some(build_user) = self.build_user() {
760 envs.push(("bob_build_user".to_string(), build_user.to_string()));
761 }
762 if let Some(bootstrap) = self.bootstrap() {
763 envs.push((
764 "bob_bootstrap".to_string(),
765 format!("{}", bootstrap.display()),
766 ));
767 }
768 envs
769 }
770
771 pub fn validate(&self) -> Result<(), Vec<String>> {
773 let mut errors: Vec<String> = Vec::new();
774
775 if !self.file.pkgsrc.basedir.exists() {
777 errors.push(format!(
778 "pkgsrc basedir does not exist: {}",
779 self.file.pkgsrc.basedir.display()
780 ));
781 }
782
783 if self.file.sandboxes.is_none() && !self.file.pkgsrc.make.exists() {
786 errors.push(format!(
787 "make binary does not exist: {}",
788 self.file.pkgsrc.make.display()
789 ));
790 }
791
792 for (name, path) in &self.file.scripts {
794 if !path.exists() {
795 errors.push(format!(
796 "Script '{}' does not exist: {}",
797 name,
798 path.display()
799 ));
800 } else if !path.is_file() {
801 errors.push(format!(
802 "Script '{}' is not a file: {}",
803 name,
804 path.display()
805 ));
806 }
807 }
808
809 if let Some(sandboxes) = &self.file.sandboxes {
811 if let Some(parent) = sandboxes.basedir.parent() {
813 if !parent.exists() {
814 errors.push(format!(
815 "Sandbox basedir parent does not exist: {}",
816 parent.display()
817 ));
818 }
819 }
820 }
821
822 if let Some(parent) = self.file.pkgsrc.logdir.parent() {
824 if !parent.exists() {
825 errors.push(format!(
826 "logdir parent directory does not exist: {}",
827 parent.display()
828 ));
829 }
830 }
831
832 if errors.is_empty() { Ok(()) } else { Err(errors) }
833 }
834}
835
836fn load_lua(filename: &Path) -> Result<(ConfigFile, LuaEnv), String> {
838 let lua = Lua::new();
839
840 if let Some(config_dir) = filename.parent() {
842 let path_setup = format!(
843 "package.path = '{}' .. '/?.lua;' .. package.path",
844 config_dir.display()
845 );
846 lua.load(&path_setup)
847 .exec()
848 .map_err(|e| format!("Failed to set package.path: {}", e))?;
849 }
850
851 lua.load(filename)
852 .exec()
853 .map_err(|e| format!("Lua execution error: {}", e))?;
854
855 let globals = lua.globals();
857
858 let options = parse_options(&globals)
860 .map_err(|e| format!("Error parsing options config: {}", e))?;
861 let pkgsrc_table: Table = globals
862 .get("pkgsrc")
863 .map_err(|e| format!("Error getting pkgsrc config: {}", e))?;
864 let pkgsrc = parse_pkgsrc(&globals)
865 .map_err(|e| format!("Error parsing pkgsrc config: {}", e))?;
866 let scripts = parse_scripts(&globals)
867 .map_err(|e| format!("Error parsing scripts config: {}", e))?;
868 let sandboxes = parse_sandboxes(&globals)
869 .map_err(|e| format!("Error parsing sandboxes config: {}", e))?;
870
871 let env_key = if let Ok(env_value) = pkgsrc_table.get::<Value>("env") {
873 if !env_value.is_nil() {
874 let key = lua.create_registry_value(env_value).map_err(|e| {
875 format!("Failed to store env in registry: {}", e)
876 })?;
877 Some(Arc::new(key))
878 } else {
879 None
880 }
881 } else {
882 None
883 };
884
885 let lua_env = LuaEnv { lua: Arc::new(Mutex::new(lua)), env_key };
886
887 let config = ConfigFile { options, pkgsrc, scripts, sandboxes };
888
889 Ok((config, lua_env))
890}
891
892fn parse_options(globals: &Table) -> LuaResult<Option<Options>> {
893 let options: Value = globals.get("options")?;
894 if options.is_nil() {
895 return Ok(None);
896 }
897
898 let table = options
899 .as_table()
900 .ok_or_else(|| mlua::Error::runtime("'options' must be a table"))?;
901
902 const KNOWN_KEYS: &[&str] =
903 &["build_threads", "scan_threads", "strict_scan", "log_level"];
904 warn_unknown_keys(table, "options", KNOWN_KEYS);
905
906 Ok(Some(Options {
907 build_threads: table.get("build_threads").ok(),
908 scan_threads: table.get("scan_threads").ok(),
909 strict_scan: table.get("strict_scan").ok(),
910 log_level: table.get("log_level").ok(),
911 }))
912}
913
914fn warn_unknown_keys(table: &Table, table_name: &str, known_keys: &[&str]) {
916 for (key, _) in table.pairs::<String, Value>().flatten() {
917 if !known_keys.contains(&key.as_str()) {
918 eprintln!("Warning: unknown config key '{}.{}'", table_name, key);
919 }
920 }
921}
922
923fn get_required_string(table: &Table, field: &str) -> LuaResult<String> {
924 let value: Value = table.get(field)?;
925 match value {
926 Value::String(s) => Ok(s.to_str()?.to_string()),
927 Value::Integer(n) => Ok(n.to_string()),
928 Value::Number(n) => Ok(n.to_string()),
929 Value::Nil => Err(mlua::Error::runtime(format!(
930 "missing required field '{}'",
931 field
932 ))),
933 _ => Err(mlua::Error::runtime(format!(
934 "field '{}' must be a string, got {}",
935 field,
936 value.type_name()
937 ))),
938 }
939}
940
941fn parse_pkgsrc(globals: &Table) -> LuaResult<Pkgsrc> {
942 let pkgsrc: Table = globals.get("pkgsrc")?;
943
944 const KNOWN_KEYS: &[&str] = &[
945 "basedir",
946 "bootstrap",
947 "build_user",
948 "cachevars",
949 "env",
950 "logdir",
951 "make",
952 "pkgpaths",
953 "save_wrkdir_patterns",
954 "tar",
955 ];
956 warn_unknown_keys(&pkgsrc, "pkgsrc", KNOWN_KEYS);
957
958 let basedir = get_required_string(&pkgsrc, "basedir")?;
959 let bootstrap: Option<PathBuf> =
960 pkgsrc.get::<Option<String>>("bootstrap")?.map(PathBuf::from);
961 let build_user: Option<String> =
962 pkgsrc.get::<Option<String>>("build_user")?;
963 let logdir = get_required_string(&pkgsrc, "logdir")?;
964 let make = get_required_string(&pkgsrc, "make")?;
965 let tar: Option<PathBuf> =
966 pkgsrc.get::<Option<String>>("tar")?.map(PathBuf::from);
967
968 let pkgpaths: Option<Vec<PkgPath>> =
969 match pkgsrc.get::<Value>("pkgpaths")? {
970 Value::Nil => None,
971 Value::Table(t) => {
972 let paths: Vec<PkgPath> = t
973 .sequence_values::<String>()
974 .filter_map(|r| r.ok())
975 .filter_map(|s| PkgPath::new(&s).ok())
976 .collect();
977 if paths.is_empty() { None } else { Some(paths) }
978 }
979 _ => None,
980 };
981
982 let save_wrkdir_patterns: Vec<String> =
983 match pkgsrc.get::<Value>("save_wrkdir_patterns")? {
984 Value::Nil => Vec::new(),
985 Value::Table(t) => {
986 t.sequence_values::<String>().filter_map(|r| r.ok()).collect()
987 }
988 _ => Vec::new(),
989 };
990
991 let cachevars: Vec<String> = match pkgsrc.get::<Value>("cachevars")? {
992 Value::Nil => Vec::new(),
993 Value::Table(t) => {
994 t.sequence_values::<String>().filter_map(|r| r.ok()).collect()
995 }
996 _ => Vec::new(),
997 };
998
999 Ok(Pkgsrc {
1000 basedir: PathBuf::from(basedir),
1001 bootstrap,
1002 build_user,
1003 cachevars,
1004 logdir: PathBuf::from(logdir),
1005 make: PathBuf::from(make),
1006 pkgpaths,
1007 save_wrkdir_patterns,
1008 tar,
1009 })
1010}
1011
1012fn parse_scripts(globals: &Table) -> LuaResult<HashMap<String, PathBuf>> {
1013 let scripts: Value = globals.get("scripts")?;
1014 if scripts.is_nil() {
1015 return Ok(HashMap::new());
1016 }
1017
1018 let table = scripts
1019 .as_table()
1020 .ok_or_else(|| mlua::Error::runtime("'scripts' must be a table"))?;
1021
1022 let mut result = HashMap::new();
1023 for pair in table.pairs::<String, String>() {
1024 let (k, v) = pair?;
1025 result.insert(k, PathBuf::from(v));
1026 }
1027
1028 Ok(result)
1029}
1030
1031fn parse_sandboxes(globals: &Table) -> LuaResult<Option<Sandboxes>> {
1032 let sandboxes: Value = globals.get("sandboxes")?;
1033 if sandboxes.is_nil() {
1034 return Ok(None);
1035 }
1036
1037 let table = sandboxes
1038 .as_table()
1039 .ok_or_else(|| mlua::Error::runtime("'sandboxes' must be a table"))?;
1040
1041 const KNOWN_KEYS: &[&str] = &["actions", "basedir"];
1042 warn_unknown_keys(table, "sandboxes", KNOWN_KEYS);
1043
1044 let basedir: String = table.get("basedir")?;
1045
1046 let actions_value: Value = table.get("actions")?;
1047 let actions = if actions_value.is_nil() {
1048 Vec::new()
1049 } else {
1050 let actions_table = actions_value.as_table().ok_or_else(|| {
1051 mlua::Error::runtime("'sandboxes.actions' must be a table")
1052 })?;
1053 parse_actions(actions_table)?
1054 };
1055
1056 Ok(Some(Sandboxes { basedir: PathBuf::from(basedir), actions }))
1057}
1058
1059fn parse_actions(table: &Table) -> LuaResult<Vec<Action>> {
1060 table.sequence_values::<Table>().map(|v| Action::from_lua(&v?)).collect()
1061}