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::ffi::{CStr, CString};
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, id: Option<usize>) -> 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(id, &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.depends()
.filter_map(|d| d.ok())
.map(|d| d.pkgpath().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,
dbdir: PathBuf,
logdir: PathBuf,
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>,
pub dynamic: Option<DynamicConfig>,
}
#[derive(Clone, Debug, Default)]
pub struct Options {
pub build_threads: Option<usize>,
pub dbdir: Option<PathBuf>,
pub scan_threads: Option<usize>,
pub strict_scan: Option<bool>,
pub log_level: Option<String>,
pub tui: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct DynamicConfig {
pub jobs: Option<usize>,
pub wrkobjdir: Option<WrkObjDir>,
}
#[derive(Clone, Debug)]
pub struct WrkObjDir {
pub tmpfs: Option<PathBuf>,
pub disk: Option<PathBuf>,
pub threshold: Option<u64>,
}
impl WrkObjDir {
pub fn route(&self, disk_usage: Option<u64>) -> Option<WrkObjKind> {
match (&self.tmpfs, &self.disk, self.threshold) {
(Some(tmpfs), Some(disk), Some(threshold)) => match disk_usage {
Some(size) if size <= threshold => Some(WrkObjKind::Tmpfs(tmpfs.clone())),
_ => Some(WrkObjKind::Disk(disk.clone())),
},
(Some(dir), None, _) => Some(WrkObjKind::Tmpfs(dir.clone())),
(None, Some(dir), _) => Some(WrkObjKind::Disk(dir.clone())),
_ => None,
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, strum::Display, strum::EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum WrkObjKind {
Tmpfs(PathBuf),
Disk(PathBuf),
}
impl WrkObjKind {
pub fn path(&self) -> &Path {
match self {
Self::Tmpfs(p) | Self::Disk(p) => p,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Pkgsrc {
pub basedir: PathBuf,
pub bootstrap: Option<PathBuf>,
pub build_user: Option<String>,
pub build_user_home: Option<PathBuf>,
pub logdir: Option<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 {
if path.is_relative() {
std::env::current_dir()
.context("Unable to determine current directory")?
.join(path)
} else {
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 raw_dbdir = file.options.as_ref().and_then(|o| o.dbdir.clone());
let dbdir = match raw_dbdir {
Some(p) if p.is_absolute() => p,
Some(p) => base_dir.join(p),
None => base_dir.join("db"),
};
let logdir = file
.pkgsrc
.logdir
.clone()
.unwrap_or_else(|| dbdir.join("logs"));
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,
dbdir,
logdir,
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 jobs(&self) -> Option<usize> {
self.file.dynamic.as_ref().and_then(|s| s.jobs)
}
pub fn wrkobjdir(&self) -> Option<&WrkObjDir> {
self.file
.dynamic
.as_ref()
.and_then(|s| s.wrkobjdir.as_ref())
}
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 tui(&self) -> bool {
self.file
.options
.as_ref()
.and_then(|o| o.tui)
.unwrap_or(true)
}
pub fn dbdir(&self) -> &PathBuf {
&self.dbdir
}
pub fn logdir(&self) -> &PathBuf {
&self.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 build_user_home(&self) -> Option<&Path> {
self.file.pkgsrc.build_user_home.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(home) = self.build_user_home() {
envs.push((
"bob_build_user_home".to_string(),
home.display().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.dbdir.parent() {
if !parent.exists() {
errors.push(format!(
"dbdir parent directory does not exist: {}",
parent.display()
));
}
}
if let Some(opts) = &self.file.options {
if opts.build_threads == Some(0) {
errors.push("build_threads must be at least 1".to_string());
}
if opts.scan_threads == Some(0) {
errors.push("scan_threads must be at least 1".to_string());
}
}
if let Some(dyn_cfg) = &self.file.dynamic {
if dyn_cfg.jobs == Some(0) {
errors.push("dynamic.jobs must be at least 1".to_string());
}
if let Some(w) = &dyn_cfg.wrkobjdir {
if w.tmpfs.is_none() && w.disk.is_none() {
errors.push(
"dynamic.wrkobjdir requires at least one of tmpfs or disk".to_string(),
);
}
if w.tmpfs.is_some() && w.disk.is_some() && w.threshold.is_none() {
errors.push(
"dynamic.wrkobjdir.threshold is required when both \
tmpfs and disk are set"
.to_string(),
);
}
}
}
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 globals = lua.globals();
let pkg: Table = globals
.get("package")
.map_err(|e| format!("Failed to get package table: {}", e))?;
let existing: String = pkg
.get("path")
.map_err(|e| format!("Failed to get package.path: {}", e))?;
let new_path = format!("{}/?.lua;{}", config_dir.display(), existing);
pkg.set("path", new_path)
.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 dynamic =
parse_dynamic(&globals).map_err(|e| format!("Error parsing dynamic 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,
dynamic,
};
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",
"dbdir",
"log_level",
"scan_threads",
"tui",
"strict_scan",
];
warn_unknown_keys(table, "options", KNOWN_KEYS);
let dbdir: Option<PathBuf> = table.get::<Option<String>>("dbdir")?.map(PathBuf::from);
Ok(Some(Options {
build_threads: table.get::<Option<usize>>("build_threads")?,
dbdir,
scan_threads: table.get::<Option<usize>>("scan_threads")?,
strict_scan: table.get::<Option<bool>>("strict_scan")?,
log_level: table.get::<Option<String>>("log_level")?,
tui: table.get::<Option<bool>>("tui")?,
}))
}
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_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty size string".to_string());
}
let (num_str, multiplier) = match s.as_bytes().last() {
Some(b'K' | b'k') => (&s[..s.len() - 1], 1024u64),
Some(b'M' | b'm') => (&s[..s.len() - 1], 1024u64 * 1024),
Some(b'G' | b'g') => (&s[..s.len() - 1], 1024u64 * 1024 * 1024),
Some(b'T' | b't') => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
_ => (s, 1u64),
};
if multiplier > 1 {
let n: f64 = num_str
.parse()
.map_err(|_| format!("invalid size: '{}'", s))?;
if n < 0.0 {
return Err(format!("negative size: '{}'", s));
}
Ok((n * multiplier as f64) as u64)
} else {
s.parse::<u64>()
.map_err(|_| format!("invalid size: '{}'", s))
}
}
fn get_home_dir(username: &str) -> Result<PathBuf, String> {
let cname = CString::new(username).map_err(|_| format!("invalid username: '{}'", username))?;
let pw = unsafe { libc::getpwnam(cname.as_ptr()) };
if pw.is_null() {
return Err(format!(
"user '{}' not found in password database",
username
));
}
let home = unsafe { CStr::from_ptr((*pw).pw_dir) };
let path = home
.to_str()
.map_err(|_| format!("non-UTF-8 home directory for user '{}'", username))?;
Ok(PathBuf::from(path))
}
fn parse_dynamic(globals: &Table) -> LuaResult<Option<DynamicConfig>> {
let value: Value = globals.get("dynamic")?;
if value.is_nil() {
return Ok(None);
}
let table = value
.as_table()
.ok_or_else(|| mlua::Error::runtime("'dynamic' must be a table"))?;
const KNOWN_KEYS: &[&str] = &["jobs", "wrkobjdir"];
warn_unknown_keys(table, "dynamic", KNOWN_KEYS);
let jobs: Option<usize> = table.get::<Option<usize>>("jobs")?;
let wrkobjdir = match table.get::<Value>("wrkobjdir")? {
Value::Nil => None,
Value::Table(t) => {
const WRK_KEYS: &[&str] = &["tmpfs", "disk", "threshold"];
warn_unknown_keys(&t, "dynamic.wrkobjdir", WRK_KEYS);
let tmpfs: Option<PathBuf> = t.get::<Option<String>>("tmpfs")?.map(PathBuf::from);
let disk: Option<PathBuf> = t.get::<Option<String>>("disk")?.map(PathBuf::from);
let threshold: Option<u64> = t
.get::<Option<String>>("threshold")?
.map(|s| {
parse_size(&s).map_err(|e| {
mlua::Error::runtime(format!("dynamic.wrkobjdir.threshold: {}", e))
})
})
.transpose()?;
Some(WrkObjDir {
tmpfs,
disk,
threshold,
})
}
_ => return Err(mlua::Error::runtime("dynamic.wrkobjdir must be a table")),
};
Ok(Some(DynamicConfig { jobs, wrkobjdir }))
}
fn parse_pkgsrc(globals: &Table) -> LuaResult<Pkgsrc> {
let pkgsrc: Table = globals.get("pkgsrc")?;
const KNOWN_KEYS: &[&str] = &[
"basedir",
"bootstrap",
"build_user",
"build_user_home",
"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 build_user_home = if let Some(ref user) = build_user {
if let Some(explicit) = pkgsrc.get::<Option<String>>("build_user_home")? {
Some(PathBuf::from(explicit))
} else {
let home = get_home_dir(user)
.map_err(|e| mlua::Error::runtime(format!("pkgsrc.build_user: {}", e)))?;
pkgsrc.set("build_user_home", home.display().to_string())?;
Some(home)
}
} else {
None
};
let logdir: Option<PathBuf> = pkgsrc.get::<Option<String>>("logdir")?.map(PathBuf::from);
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 mut paths = Vec::new();
for (i, val) in t.sequence_values::<Value>().enumerate() {
let val = val.map_err(|e| {
mlua::Error::runtime(format!("pkgsrc.pkgpaths[{}]: {}", i + 1, e))
})?;
let Value::String(s) = val else {
return Err(mlua::Error::runtime(format!(
"pkgsrc.pkgpaths[{}]: expected string",
i + 1
)));
};
let s = s.to_str().map_err(|e| {
mlua::Error::runtime(format!("pkgsrc.pkgpaths[{}]: {}", i + 1, e))
})?;
match PkgPath::new(&s) {
Ok(p) => paths.push(p),
Err(e) => {
return Err(mlua::Error::runtime(format!(
"pkgsrc.pkgpaths[{}]: invalid pkgpath '{}': {}",
i + 1,
s,
e
)));
}
}
}
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,
build_user_home,
cachevars,
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,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn load_config(lua_src: &str) -> Result<Config, String> {
let dir = tempfile::tempdir().map_err(|e| e.to_string())?;
let path = dir.path().join("config.lua");
std::fs::write(&path, lua_src).map_err(|e| e.to_string())?;
Config::load(Some(&path)).map_err(|e| e.to_string())
}
const MINIMAL: &str = r#"
pkgsrc = {
basedir = "/usr/pkgsrc",
make = "/usr/bin/make",
}
"#;
fn with_options(options: &str) -> String {
format!("{MINIMAL}\noptions = {{ {options} }}")
}
fn with_dynamic(dynamic: &str) -> String {
format!("{MINIMAL}\ndynamic = {{ {dynamic} }}")
}
#[test]
fn options_valid_types() {
let cfg = load_config(&with_options("build_threads = 4, scan_threads = 2"));
assert!(cfg.is_ok());
let cfg = cfg.ok();
assert_eq!(cfg.as_ref().map(|c| c.build_threads()), Some(4));
assert_eq!(cfg.as_ref().map(|c| c.scan_threads()), Some(2));
}
#[test]
fn options_wrong_type_errors() {
let cfg = load_config(&with_options("build_threads = \"eight\""));
assert!(cfg.is_err(), "expected error, got: {:?}", cfg);
}
#[test]
fn options_missing_is_default() {
let cfg = load_config(MINIMAL);
assert!(cfg.is_ok());
let cfg = cfg.ok();
assert_eq!(cfg.as_ref().map(|c| c.build_threads()), Some(1));
}
#[test]
fn dynamic_jobs_wrong_type_errors() {
let cfg = load_config(&with_dynamic("jobs = \"lots\""));
assert!(cfg.is_err(), "expected error, got: {:?}", cfg);
}
#[test]
fn pkgpaths_valid() {
let lua = format!("{MINIMAL}\npkgsrc.pkgpaths = {{ \"devel/cmake\", \"lang/rust\" }}");
let cfg = load_config(&lua);
assert!(cfg.is_ok(), "expected ok, got: {:?}", cfg);
}
#[test]
fn pkgpaths_invalid_errors() {
let lua = format!("{MINIMAL}\npkgsrc.pkgpaths = {{ \"mail\" }}");
let cfg = load_config(&lua);
assert!(cfg.is_err(), "expected error, got: {:?}", cfg);
}
#[test]
fn pkgpaths_wrong_type_errors() {
let lua = format!("{MINIMAL}\npkgsrc.pkgpaths = {{ 42 }}");
let cfg = load_config(&lua);
assert!(cfg.is_err(), "expected error, got: {:?}", cfg);
}
}