use crate::action::Action;
use crate::sandbox::Sandbox;
use crate::scan::ResolvedPackage;
use anyhow::{Context, Result, anyhow, bail};
use mlua::{Lua, RegistryKey, Result as LuaResult, Table, Value};
use pkgsrc::PkgPath;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
pub struct PkgsrcEnv {
pub packages: PathBuf,
pub pkgtools: PathBuf,
pub prefix: PathBuf,
pub pkg_dbdir: PathBuf,
pub pkg_refcount_dbdir: PathBuf,
pub cachevars: HashMap<String, String>,
}
impl PkgsrcEnv {
pub fn fetch(config: &Config, sandbox: &Sandbox) -> Result<Self> {
const REQUIRED_VARS: &[&str] = &[
"PACKAGES",
"PKG_DBDIR",
"PKG_REFCOUNT_DBDIR",
"PKG_TOOLS_BIN",
"PREFIX",
];
let user_cachevars = config.cachevars();
let mut all_varnames: Vec<&str> = REQUIRED_VARS.to_vec();
for v in user_cachevars {
all_varnames.push(v.as_str());
}
let varnames_arg = all_varnames.join(" ");
let script = format!(
"cd {}/pkgtools/pkg_install && {} show-vars VARNAMES=\"{}\"\n",
config.pkgsrc().display(),
config.make().display(),
varnames_arg
);
let child = sandbox.execute_script(0, &script, vec![])?;
let output = child
.wait_with_output()
.context("Failed to execute bmake show-vars")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to query pkgsrc variables: {}", stderr.trim());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
if lines.len() != all_varnames.len() {
bail!(
"Expected {} variables from pkgsrc, got {}",
all_varnames.len(),
lines.len()
);
}
let mut values: HashMap<&str, &str> = HashMap::new();
for (varname, value) in all_varnames.iter().zip(&lines) {
values.insert(varname, value);
}
for varname in REQUIRED_VARS {
if values.get(varname).is_none_or(|v| v.is_empty()) {
bail!("pkgsrc returned empty value for {}", varname);
}
}
let mut cachevars: HashMap<String, String> = HashMap::new();
for varname in user_cachevars {
if let Some(value) = values.get(varname.as_str()) {
if !value.is_empty() {
cachevars.insert(varname.clone(), (*value).to_string());
}
}
}
Ok(PkgsrcEnv {
packages: PathBuf::from(values["PACKAGES"]),
pkgtools: PathBuf::from(values["PKG_TOOLS_BIN"]),
prefix: PathBuf::from(values["PREFIX"]),
pkg_dbdir: PathBuf::from(values["PKG_DBDIR"]),
pkg_refcount_dbdir: PathBuf::from(values["PKG_REFCOUNT_DBDIR"]),
cachevars,
})
}
}
#[derive(Clone)]
pub struct LuaEnv {
lua: Arc<Mutex<Lua>>,
env_key: Option<Arc<RegistryKey>>,
}
impl std::fmt::Debug for LuaEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LuaEnv")
.field("has_env", &self.env_key.is_some())
.finish()
}
}
impl Default for LuaEnv {
fn default() -> Self {
Self {
lua: Arc::new(Mutex::new(Lua::new())),
env_key: None,
}
}
}
impl LuaEnv {
pub fn get_env(&self, pkg: &ResolvedPackage) -> Result<HashMap<String, String>, String> {
let Some(env_key) = &self.env_key else {
return Ok(HashMap::new());
};
let lua = self
.lua
.lock()
.map_err(|e| format!("Lua lock error: {}", e))?;
let env_value: Value = lua
.registry_value(env_key)
.map_err(|e| format!("Failed to get env from registry: {}", e))?;
let idx = &pkg.index;
let result_table: Table = match env_value {
Value::Function(func) => {
let pkg_table = lua
.create_table()
.map_err(|e| format!("Failed to create table: {}", e))?;
pkg_table
.set("pkgname", idx.pkgname.to_string())
.map_err(|e| format!("Failed to set pkgname: {}", e))?;
pkg_table
.set("pkgpath", pkg.pkgpath.as_path().display().to_string())
.map_err(|e| format!("Failed to set pkgpath: {}", e))?;
pkg_table
.set(
"all_depends",
idx.all_depends
.as_ref()
.map(|deps| {
deps.iter()
.map(|d| d.pkgpath().as_path().display().to_string())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default(),
)
.map_err(|e| format!("Failed to set all_depends: {}", e))?;
pkg_table
.set(
"pkg_skip_reason",
idx.pkg_skip_reason.clone().unwrap_or_default(),
)
.map_err(|e| format!("Failed to set pkg_skip_reason: {}", e))?;
pkg_table
.set(
"pkg_fail_reason",
idx.pkg_fail_reason.clone().unwrap_or_default(),
)
.map_err(|e| format!("Failed to set pkg_fail_reason: {}", e))?;
pkg_table
.set(
"no_bin_on_ftp",
idx.no_bin_on_ftp.clone().unwrap_or_default(),
)
.map_err(|e| format!("Failed to set no_bin_on_ftp: {}", e))?;
pkg_table
.set("restricted", idx.restricted.clone().unwrap_or_default())
.map_err(|e| format!("Failed to set restricted: {}", e))?;
pkg_table
.set("categories", idx.categories.clone().unwrap_or_default())
.map_err(|e| format!("Failed to set categories: {}", e))?;
pkg_table
.set("maintainer", idx.maintainer.clone().unwrap_or_default())
.map_err(|e| format!("Failed to set maintainer: {}", e))?;
pkg_table
.set("use_destdir", idx.use_destdir.clone().unwrap_or_default())
.map_err(|e| format!("Failed to set use_destdir: {}", e))?;
pkg_table
.set(
"bootstrap_pkg",
idx.bootstrap_pkg.clone().unwrap_or_default(),
)
.map_err(|e| format!("Failed to set bootstrap_pkg: {}", e))?;
pkg_table
.set(
"usergroup_phase",
idx.usergroup_phase.clone().unwrap_or_default(),
)
.map_err(|e| format!("Failed to set usergroup_phase: {}", e))?;
pkg_table
.set(
"scan_depends",
idx.scan_depends
.as_ref()
.map(|deps| {
deps.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default(),
)
.map_err(|e| format!("Failed to set scan_depends: {}", e))?;
pkg_table
.set("pbulk_weight", idx.pbulk_weight.clone().unwrap_or_default())
.map_err(|e| format!("Failed to set pbulk_weight: {}", e))?;
pkg_table
.set(
"multi_version",
idx.multi_version
.as_ref()
.map(|v| v.join(" "))
.unwrap_or_default(),
)
.map_err(|e| format!("Failed to set multi_version: {}", e))?;
pkg_table
.set(
"depends",
pkg.depends()
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(" "),
)
.map_err(|e| format!("Failed to set depends: {}", e))?;
func.call(pkg_table)
.map_err(|e| format!("Failed to call env function: {}", e))?
}
Value::Table(t) => t,
Value::Nil => return Ok(HashMap::new()),
_ => return Err("env must be a function or table".to_string()),
};
let mut env = HashMap::new();
for pair in result_table.pairs::<String, String>() {
let (k, v) = pair.map_err(|e| format!("Failed to iterate env table: {}", e))?;
env.insert(k, v);
}
Ok(env)
}
}
#[derive(Clone, Debug, Default)]
pub struct Config {
file: ConfigFile,
log_level: String,
lua_env: LuaEnv,
}
#[derive(Clone, Debug, Default)]
pub struct ConfigFile {
pub options: Option<Options>,
pub pkgsrc: Pkgsrc,
pub scripts: HashMap<String, PathBuf>,
pub sandboxes: Option<Sandboxes>,
pub environment: Option<Environment>,
}
#[derive(Clone, Debug, Default)]
pub struct Options {
pub build_threads: Option<usize>,
pub scan_threads: Option<usize>,
pub strict_scan: Option<bool>,
pub log_level: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct Pkgsrc {
pub basedir: PathBuf,
pub bootstrap: Option<PathBuf>,
pub build_user: Option<String>,
pub logdir: PathBuf,
pub make: PathBuf,
pub pkgpaths: Option<Vec<PkgPath>>,
pub save_wrkdir_patterns: Vec<String>,
pub cachevars: Vec<String>,
pub tar: Option<PathBuf>,
}
#[derive(Clone, Debug)]
pub struct Environment {
pub clear: bool,
pub inherit: Vec<String>,
pub set: HashMap<String, String>,
}
impl Default for Environment {
fn default() -> Self {
Self {
clear: true,
inherit: Vec::new(),
set: HashMap::new(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Sandboxes {
pub basedir: PathBuf,
pub actions: Vec<Action>,
pub bindfs: String,
}
impl Config {
pub fn load(config_path: Option<&Path>) -> Result<Config> {
let filename = if let Some(path) = config_path {
path.to_path_buf()
} else {
std::env::current_dir()
.context("Unable to determine current directory")?
.join("config.lua")
};
if !filename.exists() {
anyhow::bail!("Configuration file {} does not exist", filename.display());
}
let (mut file, lua_env) =
load_lua(&filename)
.map_err(|e| anyhow!(e))
.with_context(|| {
format!(
"Unable to parse Lua configuration file {}",
filename.display()
)
})?;
let base_dir = filename.parent().unwrap_or_else(|| Path::new("."));
let mut newscripts: HashMap<String, PathBuf> = HashMap::new();
for (k, v) in &file.scripts {
let fullpath = if v.is_relative() {
base_dir.join(v)
} else {
v.clone()
};
newscripts.insert(k.clone(), fullpath);
}
file.scripts = newscripts;
if let Some(ref bootstrap) = file.pkgsrc.bootstrap {
if !bootstrap.exists() {
anyhow::bail!(
"pkgsrc.bootstrap file {} does not exist",
bootstrap.display()
);
}
}
let log_level = if let Some(opts) = &file.options {
opts.log_level.clone().unwrap_or_else(|| "info".to_string())
} else {
"info".to_string()
};
Ok(Config {
file,
log_level,
lua_env,
})
}
pub fn build_threads(&self) -> usize {
if let Some(opts) = &self.file.options {
opts.build_threads.unwrap_or(1)
} else {
1
}
}
pub fn scan_threads(&self) -> usize {
if let Some(opts) = &self.file.options {
opts.scan_threads.unwrap_or(1)
} else {
1
}
}
pub fn strict_scan(&self) -> bool {
if let Some(opts) = &self.file.options {
opts.strict_scan.unwrap_or(false)
} else {
false
}
}
pub fn script(&self, key: &str) -> Option<&PathBuf> {
self.file.scripts.get(key)
}
pub fn make(&self) -> &PathBuf {
&self.file.pkgsrc.make
}
pub fn pkgpaths(&self) -> &Option<Vec<PkgPath>> {
&self.file.pkgsrc.pkgpaths
}
pub fn pkgsrc(&self) -> &PathBuf {
&self.file.pkgsrc.basedir
}
pub fn sandboxes(&self) -> &Option<Sandboxes> {
&self.file.sandboxes
}
pub fn environment(&self) -> Option<&Environment> {
self.file.environment.as_ref()
}
pub fn bindfs(&self) -> &str {
self.file
.sandboxes
.as_ref()
.map(|s| s.bindfs.as_str())
.unwrap_or("bindfs")
}
pub fn log_level(&self) -> &str {
&self.log_level
}
pub fn logdir(&self) -> &PathBuf {
&self.file.pkgsrc.logdir
}
pub fn save_wrkdir_patterns(&self) -> &[String] {
self.file.pkgsrc.save_wrkdir_patterns.as_slice()
}
pub fn tar(&self) -> Option<&PathBuf> {
self.file.pkgsrc.tar.as_ref()
}
pub fn build_user(&self) -> Option<&str> {
self.file.pkgsrc.build_user.as_deref()
}
pub fn bootstrap(&self) -> Option<&PathBuf> {
self.file.pkgsrc.bootstrap.as_ref()
}
pub fn cachevars(&self) -> &[String] {
self.file.pkgsrc.cachevars.as_slice()
}
pub fn get_pkg_env(
&self,
pkg: &ResolvedPackage,
) -> Result<std::collections::HashMap<String, String>, String> {
self.lua_env.get_env(pkg)
}
pub fn script_env(&self, pkgsrc_env: Option<&PkgsrcEnv>) -> Vec<(String, String)> {
let mut envs = vec![
(
"bob_logdir".to_string(),
format!("{}", self.logdir().display()),
),
("bob_make".to_string(), format!("{}", self.make().display())),
(
"bob_pkgsrc".to_string(),
format!("{}", self.pkgsrc().display()),
),
];
if let Some(env) = pkgsrc_env {
envs.push((
"bob_packages".to_string(),
env.packages.display().to_string(),
));
envs.push((
"bob_pkgtools".to_string(),
env.pkgtools.display().to_string(),
));
envs.push(("bob_prefix".to_string(), env.prefix.display().to_string()));
envs.push((
"bob_pkg_dbdir".to_string(),
env.pkg_dbdir.display().to_string(),
));
envs.push((
"bob_pkg_refcount_dbdir".to_string(),
env.pkg_refcount_dbdir.display().to_string(),
));
for (key, value) in &env.cachevars {
envs.push((key.clone(), value.clone()));
}
}
let tar_value = self
.tar()
.map(|t| t.display().to_string())
.unwrap_or_else(|| "tar".to_string());
envs.push(("bob_tar".to_string(), tar_value));
if let Some(build_user) = self.build_user() {
envs.push(("bob_build_user".to_string(), build_user.to_string()));
}
if let Some(bootstrap) = self.bootstrap() {
envs.push((
"bob_bootstrap".to_string(),
format!("{}", bootstrap.display()),
));
}
envs
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors: Vec<String> = Vec::new();
if !self.file.pkgsrc.basedir.exists() {
errors.push(format!(
"pkgsrc basedir does not exist: {}",
self.file.pkgsrc.basedir.display()
));
}
if self.file.sandboxes.is_none() && !self.file.pkgsrc.make.exists() {
errors.push(format!(
"make binary does not exist: {}",
self.file.pkgsrc.make.display()
));
}
for (name, path) in &self.file.scripts {
if !path.exists() {
errors.push(format!(
"Script '{}' does not exist: {}",
name,
path.display()
));
} else if !path.is_file() {
errors.push(format!(
"Script '{}' is not a file: {}",
name,
path.display()
));
}
}
if let Some(sandboxes) = &self.file.sandboxes {
if let Some(parent) = sandboxes.basedir.parent() {
if !parent.exists() {
errors.push(format!(
"Sandbox basedir parent does not exist: {}",
parent.display()
));
}
}
}
if let Some(parent) = self.file.pkgsrc.logdir.parent() {
if !parent.exists() {
errors.push(format!(
"logdir parent directory does not exist: {}",
parent.display()
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
fn load_lua(filename: &Path) -> Result<(ConfigFile, LuaEnv), String> {
let lua = Lua::new();
if let Some(config_dir) = filename.parent() {
let path_setup = format!(
"package.path = '{}' .. '/?.lua;' .. package.path",
config_dir.display()
);
lua.load(&path_setup)
.exec()
.map_err(|e| format!("Failed to set package.path: {}", e))?;
}
lua.load(include_str!("funcs.lua"))
.exec()
.map_err(|e| format!("Failed to load helper functions: {}", e))?;
lua.load(filename)
.exec()
.map_err(|e| format!("Lua execution error: {}", e))?;
let globals = lua.globals();
let options =
parse_options(&globals).map_err(|e| format!("Error parsing options config: {}", e))?;
let pkgsrc_table: Table = globals
.get("pkgsrc")
.map_err(|e| format!("Error getting pkgsrc config: {}", e))?;
let pkgsrc =
parse_pkgsrc(&globals).map_err(|e| format!("Error parsing pkgsrc config: {}", e))?;
let scripts =
parse_scripts(&globals).map_err(|e| format!("Error parsing scripts config: {}", e))?;
let sandboxes =
parse_sandboxes(&globals).map_err(|e| format!("Error parsing sandboxes config: {}", e))?;
let environment = parse_environment(&globals)
.map_err(|e| format!("Error parsing environment config: {}", e))?;
let env_key = if let Ok(env_value) = pkgsrc_table.get::<Value>("env") {
if !env_value.is_nil() {
let key = lua
.create_registry_value(env_value)
.map_err(|e| format!("Failed to store env in registry: {}", e))?;
Some(Arc::new(key))
} else {
None
}
} else {
None
};
let lua_env = LuaEnv {
lua: Arc::new(Mutex::new(lua)),
env_key,
};
let config = ConfigFile {
options,
pkgsrc,
scripts,
sandboxes,
environment,
};
Ok((config, lua_env))
}
fn parse_options(globals: &Table) -> LuaResult<Option<Options>> {
let options: Value = globals.get("options")?;
if options.is_nil() {
return Ok(None);
}
let table = options
.as_table()
.ok_or_else(|| mlua::Error::runtime("'options' must be a table"))?;
const KNOWN_KEYS: &[&str] = &["build_threads", "scan_threads", "strict_scan", "log_level"];
warn_unknown_keys(table, "options", KNOWN_KEYS);
Ok(Some(Options {
build_threads: table.get("build_threads").ok(),
scan_threads: table.get("scan_threads").ok(),
strict_scan: table.get("strict_scan").ok(),
log_level: table.get("log_level").ok(),
}))
}
fn warn_unknown_keys(table: &Table, table_name: &str, known_keys: &[&str]) {
for (key, _) in table.pairs::<String, Value>().flatten() {
if !known_keys.contains(&key.as_str()) {
eprintln!("Warning: unknown config key '{}.{}'", table_name, key);
}
}
}
fn get_required_string(table: &Table, field: &str) -> LuaResult<String> {
let value: Value = table.get(field)?;
match value {
Value::String(s) => Ok(s.to_str()?.to_string()),
Value::Integer(n) => Ok(n.to_string()),
Value::Number(n) => Ok(n.to_string()),
Value::Nil => Err(mlua::Error::runtime(format!(
"missing required field '{}'",
field
))),
_ => Err(mlua::Error::runtime(format!(
"field '{}' must be a string, got {}",
field,
value.type_name()
))),
}
}
fn parse_pkgsrc(globals: &Table) -> LuaResult<Pkgsrc> {
let pkgsrc: Table = globals.get("pkgsrc")?;
const KNOWN_KEYS: &[&str] = &[
"basedir",
"bootstrap",
"build_user",
"cachevars",
"env",
"logdir",
"make",
"pkgpaths",
"save_wrkdir_patterns",
"tar",
];
warn_unknown_keys(&pkgsrc, "pkgsrc", KNOWN_KEYS);
let basedir = get_required_string(&pkgsrc, "basedir")?;
let bootstrap: Option<PathBuf> = pkgsrc
.get::<Option<String>>("bootstrap")?
.map(PathBuf::from);
let build_user: Option<String> = pkgsrc.get::<Option<String>>("build_user")?;
let logdir = get_required_string(&pkgsrc, "logdir")?;
let make = get_required_string(&pkgsrc, "make")?;
let tar: Option<PathBuf> = pkgsrc.get::<Option<String>>("tar")?.map(PathBuf::from);
let pkgpaths: Option<Vec<PkgPath>> = match pkgsrc.get::<Value>("pkgpaths")? {
Value::Nil => None,
Value::Table(t) => {
let paths: Vec<PkgPath> = t
.sequence_values::<String>()
.filter_map(|r| r.ok())
.filter_map(|s| PkgPath::new(&s).ok())
.collect();
if paths.is_empty() { None } else { Some(paths) }
}
_ => None,
};
let save_wrkdir_patterns: Vec<String> = match pkgsrc.get::<Value>("save_wrkdir_patterns")? {
Value::Nil => Vec::new(),
Value::Table(t) => t
.sequence_values::<String>()
.filter_map(|r| r.ok())
.collect(),
_ => Vec::new(),
};
let cachevars: Vec<String> = match pkgsrc.get::<Value>("cachevars")? {
Value::Nil => Vec::new(),
Value::Table(t) => t
.sequence_values::<String>()
.filter_map(|r| r.ok())
.collect(),
_ => Vec::new(),
};
Ok(Pkgsrc {
basedir: PathBuf::from(basedir),
bootstrap,
build_user,
cachevars,
logdir: PathBuf::from(logdir),
make: PathBuf::from(make),
pkgpaths,
save_wrkdir_patterns,
tar,
})
}
fn parse_scripts(globals: &Table) -> LuaResult<HashMap<String, PathBuf>> {
let scripts: Value = globals.get("scripts")?;
if scripts.is_nil() {
return Ok(HashMap::new());
}
let table = scripts
.as_table()
.ok_or_else(|| mlua::Error::runtime("'scripts' must be a table"))?;
let mut result = HashMap::new();
for pair in table.pairs::<String, String>() {
let (k, v) = pair?;
result.insert(k, PathBuf::from(v));
}
Ok(result)
}
fn parse_sandboxes(globals: &Table) -> LuaResult<Option<Sandboxes>> {
let sandboxes: Value = globals.get("sandboxes")?;
if sandboxes.is_nil() {
return Ok(None);
}
let table = sandboxes
.as_table()
.ok_or_else(|| mlua::Error::runtime("'sandboxes' must be a table"))?;
const KNOWN_KEYS: &[&str] = &["actions", "basedir", "bindfs"];
warn_unknown_keys(table, "sandboxes", KNOWN_KEYS);
let basedir: String = table.get("basedir")?;
let bindfs: String = table
.get::<Option<String>>("bindfs")?
.unwrap_or_else(|| String::from("bindfs"));
let actions_value: Value = table.get("actions")?;
let actions = if actions_value.is_nil() {
Vec::new()
} else {
let actions_table = actions_value
.as_table()
.ok_or_else(|| mlua::Error::runtime("'sandboxes.actions' must be a table"))?;
parse_actions(actions_table, globals)?
};
Ok(Some(Sandboxes {
basedir: PathBuf::from(basedir),
actions,
bindfs,
}))
}
fn parse_actions(table: &Table, globals: &Table) -> LuaResult<Vec<Action>> {
let mut actions = Vec::new();
for v in table.sequence_values::<Table>() {
let mut action = Action::from_lua(&v?)?;
if let Some(varpath) = action.ifset().map(String::from) {
match resolve_lua_var(globals, &varpath) {
Some(val) => action.substitute_var(&varpath, &val),
None => continue,
}
}
actions.push(action);
}
Ok(actions)
}
fn resolve_lua_var(globals: &Table, path: &str) -> Option<String> {
let mut parts = path.split('.');
let first = parts.next()?;
let mut current: Value = globals.get(first).ok()?;
for key in parts {
match current {
Value::Table(t) => {
current = t.get(key).ok()?;
}
_ => return None,
}
}
match current {
Value::String(s) => Some(s.to_str().ok()?.to_string()),
Value::Integer(n) => Some(n.to_string()),
Value::Number(n) => Some(n.to_string()),
_ => None,
}
}
fn parse_environment(globals: &Table) -> LuaResult<Option<Environment>> {
let environment: Value = globals.get("environment")?;
if environment.is_nil() {
return Ok(None);
}
let table = environment
.as_table()
.ok_or_else(|| mlua::Error::runtime("'environment' must be a table"))?;
const KNOWN_KEYS: &[&str] = &["clear", "inherit", "set"];
warn_unknown_keys(table, "environment", KNOWN_KEYS);
let clear: bool = table.get::<Option<bool>>("clear")?.unwrap_or(true);
let inherit: Vec<String> = match table.get::<Value>("inherit")? {
Value::Nil => Vec::new(),
Value::Table(t) => t
.sequence_values::<String>()
.filter_map(|r| r.ok())
.collect(),
_ => {
return Err(mlua::Error::runtime(
"'environment.inherit' must be a table",
));
}
};
let set: HashMap<String, String> = match table.get::<Value>("set")? {
Value::Nil => HashMap::new(),
Value::Table(t) => {
let mut map = HashMap::new();
for pair in t.pairs::<String, String>() {
let (k, v) = pair?;
map.insert(k, v);
}
map
}
_ => return Err(mlua::Error::runtime("'environment.set' must be a table")),
};
Ok(Some(Environment {
clear,
inherit,
set,
}))
}