bob/config.rs
1/*
2 * Copyright (c) 2025 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17//! Configuration file parsing (Lua format).
18//!
19//! Bob uses Lua configuration files for maximum flexibility. The configuration
20//! defines paths to pkgsrc, packages to build, sandbox setup, and build scripts.
21//!
22//! # Configuration File Structure
23//!
24//! A configuration file has four main sections:
25//!
26//! - [`options`](#options-section) - General build options (optional)
27//! - [`pkgsrc`](#pkgsrc-section) - pkgsrc paths and package list (required)
28//! - [`scripts`](#scripts-section) - Build script paths (required)
29//! - [`sandboxes`](#sandboxes-section) - Sandbox configuration (optional)
30//!
31//! # Options Section
32//!
33//! The `options` section is optional. All fields have defaults.
34//!
35//! | Field | Type | Default | Description |
36//! |-------|------|---------|-------------|
37//! | `build_threads` | integer | 1 | Number of parallel build sandboxes. Each sandbox builds one package at a time. |
38//! | `scan_threads` | integer | 1 | Number of parallel scan processes for dependency discovery. |
39//! | `verbose` | boolean | false | Enable verbose output. Can be overridden by the `-v` command line flag. |
40//!
41//! # Pkgsrc Section
42//!
43//! The `pkgsrc` section is required and defines paths to pkgsrc components.
44//!
45//! ## Required Fields
46//!
47//! | Field | Type | Description |
48//! |-------|------|-------------|
49//! | `basedir` | string | Absolute path to the pkgsrc source tree (e.g., `/data/pkgsrc`). |
50//! | `logdir` | string | Directory for all logs. Per-package build logs go in subdirectories. Failed builds leave logs here; successful builds clean up. |
51//! | `make` | string | Absolute path to the bmake binary (e.g., `/usr/pkg/bin/bmake`). |
52//! | `packages` | string | Directory where binary packages are stored after successful builds. |
53//! | `pkgtools` | string | Directory containing `pkg_add`, `pkg_delete`, and other pkg tools (e.g., `/usr/pkg/sbin`). |
54//! | `prefix` | string | Installation prefix for packages (e.g., `/usr/pkg`). Must match the bootstrap kit. |
55//! | `tar` | string | Absolute path to a tar binary capable of extracting the bootstrap kit. |
56//!
57//! ## Optional Fields
58//!
59//! | Field | Type | Default | Description |
60//! |-------|------|---------|-------------|
61//! | `bootstrap` | string | none | Path to a bootstrap tarball. Required on non-NetBSD systems. Unpacked into each sandbox before builds. |
62//! | `build_user` | string | none | Unprivileged user to run builds as. If set, builds run as this user instead of root. |
63//! | `pkgpaths` | table | `{}` | List of package paths to build (e.g., `{"mail/mutt", "www/curl"}`). Dependencies are discovered automatically. |
64//! | `report_dir` | string | `logdir` | Directory for HTML build reports. Defaults to the `logdir` directory. |
65//! | `save_wrkdir_patterns` | table | `{}` | Glob patterns for files to preserve from WRKDIR on build failure (e.g., `{"**/config.log"}`). |
66//! | `env` | function or table | `{}` | Environment variables for builds. Can be a table of key-value pairs, or a function receiving package metadata and returning a table. See [Environment Function](#environment-function). |
67//!
68//! ## Environment Function
69//!
70//! The `env` field can be a function that returns environment variables for each
71//! package build. The function receives a `pkg` table with the following fields:
72//!
73//! | Field | Type | Description |
74//! |-------|------|-------------|
75//! | `pkgname` | string | Package name with version (e.g., `mutt-2.2.12`). |
76//! | `pkgpath` | string | Package path in pkgsrc (e.g., `mail/mutt`). |
77//! | `all_depends` | string | Space-separated list of all transitive dependency paths. |
78//! | `depends` | string | Space-separated list of direct dependency package names. |
79//! | `scan_depends` | string | Space-separated list of scan-time dependency paths. |
80//! | `categories` | string | Package categories from `CATEGORIES`. |
81//! | `maintainer` | string | Package maintainer email from `MAINTAINER`. |
82//! | `bootstrap_pkg` | string | Value of `BOOTSTRAP_PKG` if set. |
83//! | `usergroup_phase` | string | Value of `USERGROUP_PHASE` if set. |
84//! | `use_destdir` | string | Value of `USE_DESTDIR`. |
85//! | `multi_version` | string | Value of `MULTI_VERSION` if set. |
86//! | `pbulk_weight` | string | Value of `PBULK_WEIGHT` if set. |
87//! | `pkg_skip_reason` | string | Value of `PKG_SKIP_REASON` if set. |
88//! | `pkg_fail_reason` | string | Value of `PKG_FAIL_REASON` if set. |
89//! | `no_bin_on_ftp` | string | Value of `NO_BIN_ON_FTP` if set. |
90//! | `restricted` | string | Value of `RESTRICTED` if set. |
91//!
92//! # Scripts Section
93//!
94//! The `scripts` section defines paths to build scripts. Relative paths are
95//! resolved from the configuration file's directory.
96//!
97//! | Script | Required | Description |
98//! |--------|----------|-------------|
99//! | `pre-build` | no | Executed before each package build. Used for per-build sandbox setup (e.g., unpacking bootstrap kit). Receives environment variables listed in [Script Environment](#script-environment). |
100//! | `pkg-build` | yes | Main build script. Receives package metadata on stdin and environment variables. Must handle all build phases. |
101//! | `post-build` | no | Executed after each package build completes (success or failure). |
102//!
103//! ## Script Environment
104//!
105//! Build scripts receive these environment variables:
106//!
107//! | Variable | Description |
108//! |----------|-------------|
109//! | `bob_logdir` | Path to the log directory. |
110//! | `bob_make` | Path to the bmake binary. |
111//! | `bob_packages` | Path to the packages directory. |
112//! | `bob_pkgtools` | Path to the pkg tools directory. |
113//! | `bob_pkgsrc` | Path to the pkgsrc source tree. |
114//! | `bob_prefix` | Installation prefix. |
115//! | `bob_tar` | Path to the tar binary. |
116//! | `bob_build_user` | Unprivileged build user, if configured. |
117//! | `bob_bootstrap` | Path to the bootstrap tarball, if configured. |
118//! | `bob_status_fd` | File descriptor for sending status messages back to bob. |
119//!
120//! ## Status Messages
121//!
122//! Scripts can send status updates to bob by writing to the file descriptor
123//! in `bob_status_fd`:
124//!
125//! | Message | Description |
126//! |---------|-------------|
127//! | `stage:<name>` | Build entered a new phase (e.g., `stage:configure`). Displayed in the TUI. |
128//! | `skipped` | Package was skipped (e.g., already up-to-date). |
129//!
130//! # Sandboxes Section
131//!
132//! The `sandboxes` section is optional. When present, builds run in isolated
133//! chroot environments.
134//!
135//! | Field | Type | Required | Description |
136//! |-------|------|----------|-------------|
137//! | `basedir` | string | yes | Base directory for sandbox roots. Sandboxes are created as numbered subdirectories (`basedir/0`, `basedir/1`, etc.). |
138//! | `actions` | table | yes | List of actions to perform during sandbox setup. See the [`action`](crate::action) module for details. |
139
140use crate::action::Action;
141use anyhow::{Context, Result, anyhow};
142use mlua::{Lua, RegistryKey, Result as LuaResult, Table, Value};
143use pkgsrc::{PkgPath, ScanIndex};
144use std::collections::HashMap;
145use std::path::{Path, PathBuf};
146use std::sync::{Arc, Mutex};
147
148/// Holds the Lua state for evaluating env functions.
149#[derive(Clone)]
150pub struct LuaEnv {
151 lua: Arc<Mutex<Lua>>,
152 env_key: Option<Arc<RegistryKey>>,
153}
154
155impl std::fmt::Debug for LuaEnv {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 f.debug_struct("LuaEnv")
158 .field("has_env", &self.env_key.is_some())
159 .finish()
160 }
161}
162
163impl Default for LuaEnv {
164 fn default() -> Self {
165 Self { lua: Arc::new(Mutex::new(Lua::new())), env_key: None }
166 }
167}
168
169impl LuaEnv {
170 /// Get environment variables for a package by calling the env function.
171 /// Returns a HashMap of VAR_NAME -> value.
172 pub fn get_env(
173 &self,
174 idx: &ScanIndex,
175 ) -> Result<HashMap<String, String>, String> {
176 let Some(env_key) = &self.env_key else {
177 return Ok(HashMap::new());
178 };
179
180 let lua =
181 self.lua.lock().map_err(|e| format!("Lua lock error: {}", e))?;
182
183 // Get the env value from registry
184 let env_value: Value = lua
185 .registry_value(env_key)
186 .map_err(|e| format!("Failed to get env from registry: {}", e))?;
187
188 let result_table: Table = match env_value {
189 // If it's a function, call it with pkg info
190 Value::Function(func) => {
191 let pkg_table = lua
192 .create_table()
193 .map_err(|e| format!("Failed to create table: {}", e))?;
194
195 // Set all ScanIndex fields
196 pkg_table
197 .set("pkgname", idx.pkgname.pkgname())
198 .map_err(|e| format!("Failed to set pkgname: {}", e))?;
199 pkg_table
200 .set(
201 "pkgpath",
202 idx.pkg_location
203 .as_ref()
204 .map(|p| p.as_path().display().to_string())
205 .unwrap_or_default(),
206 )
207 .map_err(|e| format!("Failed to set pkgpath: {}", e))?;
208 pkg_table
209 .set(
210 "all_depends",
211 idx.all_depends
212 .iter()
213 .map(|d| {
214 d.pkgpath().as_path().display().to_string()
215 })
216 .collect::<Vec<_>>()
217 .join(" "),
218 )
219 .map_err(|e| format!("Failed to set all_depends: {}", e))?;
220 pkg_table
221 .set(
222 "pkg_skip_reason",
223 idx.pkg_skip_reason.clone().unwrap_or_default(),
224 )
225 .map_err(|e| {
226 format!("Failed to set pkg_skip_reason: {}", e)
227 })?;
228 pkg_table
229 .set(
230 "pkg_fail_reason",
231 idx.pkg_fail_reason.clone().unwrap_or_default(),
232 )
233 .map_err(|e| {
234 format!("Failed to set pkg_fail_reason: {}", e)
235 })?;
236 pkg_table
237 .set(
238 "no_bin_on_ftp",
239 idx.no_bin_on_ftp.clone().unwrap_or_default(),
240 )
241 .map_err(|e| {
242 format!("Failed to set no_bin_on_ftp: {}", e)
243 })?;
244 pkg_table
245 .set(
246 "restricted",
247 idx.restricted.clone().unwrap_or_default(),
248 )
249 .map_err(|e| format!("Failed to set restricted: {}", e))?;
250 pkg_table
251 .set(
252 "categories",
253 idx.categories.clone().unwrap_or_default(),
254 )
255 .map_err(|e| format!("Failed to set categories: {}", e))?;
256 pkg_table
257 .set(
258 "maintainer",
259 idx.maintainer.clone().unwrap_or_default(),
260 )
261 .map_err(|e| format!("Failed to set maintainer: {}", e))?;
262 pkg_table
263 .set(
264 "use_destdir",
265 idx.use_destdir.clone().unwrap_or_default(),
266 )
267 .map_err(|e| format!("Failed to set use_destdir: {}", e))?;
268 pkg_table
269 .set(
270 "bootstrap_pkg",
271 idx.bootstrap_pkg.clone().unwrap_or_default(),
272 )
273 .map_err(|e| {
274 format!("Failed to set bootstrap_pkg: {}", e)
275 })?;
276 pkg_table
277 .set(
278 "usergroup_phase",
279 idx.usergroup_phase.clone().unwrap_or_default(),
280 )
281 .map_err(|e| {
282 format!("Failed to set usergroup_phase: {}", e)
283 })?;
284 pkg_table
285 .set(
286 "scan_depends",
287 idx.scan_depends
288 .iter()
289 .map(|p| p.display().to_string())
290 .collect::<Vec<_>>()
291 .join(" "),
292 )
293 .map_err(|e| {
294 format!("Failed to set scan_depends: {}", e)
295 })?;
296 pkg_table
297 .set(
298 "pbulk_weight",
299 idx.pbulk_weight.clone().unwrap_or_default(),
300 )
301 .map_err(|e| {
302 format!("Failed to set pbulk_weight: {}", e)
303 })?;
304 pkg_table
305 .set("multi_version", idx.multi_version.join(" "))
306 .map_err(|e| {
307 format!("Failed to set multi_version: {}", e)
308 })?;
309 pkg_table
310 .set(
311 "depends",
312 idx.depends
313 .iter()
314 .map(|d| d.pkgname())
315 .collect::<Vec<_>>()
316 .join(" "),
317 )
318 .map_err(|e| format!("Failed to set depends: {}", e))?;
319
320 func.call(pkg_table).map_err(|e| {
321 format!("Failed to call env function: {}", e)
322 })?
323 }
324 // If it's a table, use it directly
325 Value::Table(t) => t,
326 Value::Nil => return Ok(HashMap::new()),
327 _ => return Err("env must be a function or table".to_string()),
328 };
329
330 // Convert Lua table to HashMap
331 let mut env = HashMap::new();
332 for pair in result_table.pairs::<String, String>() {
333 let (k, v) = pair
334 .map_err(|e| format!("Failed to iterate env table: {}", e))?;
335 env.insert(k, v);
336 }
337
338 Ok(env)
339 }
340}
341
342/// Main configuration structure.
343///
344/// Load configuration using [`Config::load`], then access settings through
345/// the provided methods.
346///
347/// # Example
348///
349/// ```no_run
350/// use bob::Config;
351/// use std::path::Path;
352///
353/// let config = Config::load(Some(Path::new("/data/bob/config.lua")), false)?;
354/// println!("Building with {} threads", config.build_threads());
355/// # Ok::<(), anyhow::Error>(())
356/// ```
357#[derive(Clone, Debug, Default)]
358pub struct Config {
359 file: ConfigFile,
360 filename: PathBuf,
361 verbose: bool,
362 lua_env: LuaEnv,
363}
364
365/// Parsed configuration file contents.
366#[derive(Clone, Debug, Default)]
367pub struct ConfigFile {
368 /// The `options` section.
369 pub options: Option<Options>,
370 /// The `pkgsrc` section.
371 pub pkgsrc: Pkgsrc,
372 /// The `scripts` section (script name -> path).
373 pub scripts: HashMap<String, PathBuf>,
374 /// The `sandboxes` section.
375 pub sandboxes: Option<Sandboxes>,
376}
377
378/// General build options from the `options` section.
379///
380/// All fields are optional; defaults are used when not specified:
381/// - `build_threads`: 1
382/// - `scan_threads`: 1
383/// - `verbose`: false
384#[derive(Clone, Debug, Default)]
385pub struct Options {
386 /// Number of parallel build sandboxes.
387 pub build_threads: Option<usize>,
388 /// Number of parallel scan processes.
389 pub scan_threads: Option<usize>,
390 /// Enable verbose output.
391 pub verbose: Option<bool>,
392}
393
394/// pkgsrc-related configuration from the `pkgsrc` section.
395///
396/// # Required Fields
397///
398/// - `basedir`: Path to pkgsrc source tree
399/// - `logdir`: Directory for logs
400/// - `make`: Path to bmake binary
401/// - `packages`: Directory for built packages
402/// - `pkgtools`: Directory containing pkg_add/pkg_delete
403/// - `prefix`: Installation prefix (e.g., `/usr/pkg`)
404/// - `tar`: Path to tar binary
405///
406/// # Optional Fields
407///
408/// - `bootstrap`: Path to bootstrap tarball (required on non-NetBSD systems)
409/// - `build_user`: Unprivileged user for builds
410/// - `pkgpaths`: List of packages to build
411/// - `report_dir`: Directory for HTML reports
412/// - `save_wrkdir_patterns`: Glob patterns for files to save on build failure
413#[derive(Clone, Debug, Default)]
414pub struct Pkgsrc {
415 /// Path to pkgsrc source tree.
416 pub basedir: PathBuf,
417 /// Path to bootstrap tarball (required on non-NetBSD).
418 pub bootstrap: Option<PathBuf>,
419 /// Unprivileged user for builds.
420 pub build_user: Option<String>,
421 /// Directory for logs.
422 pub logdir: PathBuf,
423 /// Path to bmake binary.
424 pub make: PathBuf,
425 /// Directory for built packages.
426 pub packages: PathBuf,
427 /// Directory containing pkg_add/pkg_delete.
428 pub pkgtools: PathBuf,
429 /// List of packages to build.
430 pub pkgpaths: Option<Vec<PkgPath>>,
431 /// Installation prefix.
432 pub prefix: PathBuf,
433 /// Directory for HTML reports.
434 pub report_dir: Option<PathBuf>,
435 /// Glob patterns for files to save from WRKDIR on failure.
436 pub save_wrkdir_patterns: Vec<String>,
437 /// Path to tar binary.
438 pub tar: PathBuf,
439}
440
441/// Sandbox configuration from the `sandboxes` section.
442///
443/// When this section is present in the configuration, builds are performed
444/// in isolated chroot environments.
445///
446/// # Example
447///
448/// ```lua
449/// sandboxes = {
450/// basedir = "/data/chroot/bob",
451/// actions = {
452/// { action = "mount", fs = "proc", dir = "/proc" },
453/// { action = "copy", dir = "/etc" },
454/// },
455/// }
456/// ```
457#[derive(Clone, Debug, Default)]
458pub struct Sandboxes {
459 /// Base directory for sandbox roots (e.g., `/data/chroot/bob`).
460 ///
461 /// Individual sandboxes are created as numbered subdirectories:
462 /// `basedir/0`, `basedir/1`, etc.
463 pub basedir: PathBuf,
464 /// Actions to perform during sandbox setup/teardown.
465 ///
466 /// See [`Action`] for details.
467 pub actions: Vec<Action>,
468}
469
470impl Config {
471 /// Load configuration from a Lua file.
472 ///
473 /// # Arguments
474 ///
475 /// * `config_path` - Path to configuration file, or `None` to use `./config.lua`
476 /// * `verbose` - Enable verbose output (overrides config file setting)
477 ///
478 /// # Errors
479 ///
480 /// Returns an error if the configuration file doesn't exist or contains
481 /// invalid Lua syntax.
482 pub fn load(config_path: Option<&Path>, verbose: bool) -> Result<Config> {
483 /*
484 * Load user-supplied configuration file, or the default location.
485 */
486 let filename = if let Some(path) = config_path {
487 path.to_path_buf()
488 } else {
489 std::env::current_dir()
490 .context("Unable to determine current directory")?
491 .join("config.lua")
492 };
493
494 /* A configuration file is mandatory. */
495 if !filename.exists() {
496 anyhow::bail!(
497 "Configuration file {} does not exist",
498 filename.display()
499 );
500 }
501
502 /*
503 * Parse configuration file as Lua.
504 */
505 let (mut file, lua_env) =
506 load_lua(&filename).map_err(|e| anyhow!(e)).with_context(|| {
507 format!(
508 "Unable to parse Lua configuration file {}",
509 filename.display()
510 )
511 })?;
512
513 /*
514 * Parse scripts section. Paths are resolved relative to config dir
515 * if not absolute.
516 */
517 let base_dir = filename.parent().unwrap_or_else(|| Path::new("."));
518 let mut newscripts: HashMap<String, PathBuf> = HashMap::new();
519 for (k, v) in &file.scripts {
520 let fullpath =
521 if v.is_relative() { base_dir.join(v) } else { v.clone() };
522 newscripts.insert(k.clone(), fullpath);
523 }
524 file.scripts = newscripts;
525
526 /*
527 * Validate bootstrap path exists if specified.
528 */
529 if let Some(ref bootstrap) = file.pkgsrc.bootstrap {
530 if !bootstrap.exists() {
531 anyhow::bail!(
532 "pkgsrc.bootstrap file {} does not exist",
533 bootstrap.display()
534 );
535 }
536 }
537
538 /*
539 * Set verbose from command line option, falling back to config file.
540 */
541 let verbose = if verbose {
542 true
543 } else if let Some(v) = &file.options {
544 v.verbose.unwrap_or(false)
545 } else {
546 false
547 };
548
549 Ok(Config { file, filename, verbose, lua_env })
550 }
551
552 pub fn build_threads(&self) -> usize {
553 if let Some(opts) = &self.file.options {
554 opts.build_threads.unwrap_or(1)
555 } else {
556 1
557 }
558 }
559
560 pub fn scan_threads(&self) -> usize {
561 if let Some(opts) = &self.file.options {
562 opts.scan_threads.unwrap_or(1)
563 } else {
564 1
565 }
566 }
567
568 pub fn script(&self, key: &str) -> Option<&PathBuf> {
569 self.file.scripts.get(key)
570 }
571
572 pub fn make(&self) -> &PathBuf {
573 &self.file.pkgsrc.make
574 }
575
576 pub fn pkgpaths(&self) -> &Option<Vec<PkgPath>> {
577 &self.file.pkgsrc.pkgpaths
578 }
579
580 pub fn pkgsrc(&self) -> &PathBuf {
581 &self.file.pkgsrc.basedir
582 }
583
584 pub fn sandboxes(&self) -> &Option<Sandboxes> {
585 &self.file.sandboxes
586 }
587
588 pub fn verbose(&self) -> bool {
589 self.verbose
590 }
591
592 /// Return the path to the configuration file.
593 pub fn config_path(&self) -> Option<&Path> {
594 if self.filename.as_os_str().is_empty() {
595 None
596 } else {
597 Some(&self.filename)
598 }
599 }
600
601 pub fn logdir(&self) -> &PathBuf {
602 &self.file.pkgsrc.logdir
603 }
604
605 pub fn packages(&self) -> &PathBuf {
606 &self.file.pkgsrc.packages
607 }
608
609 pub fn pkgtools(&self) -> &PathBuf {
610 &self.file.pkgsrc.pkgtools
611 }
612
613 pub fn prefix(&self) -> &PathBuf {
614 &self.file.pkgsrc.prefix
615 }
616
617 #[allow(dead_code)]
618 pub fn report_dir(&self) -> Option<&PathBuf> {
619 self.file.pkgsrc.report_dir.as_ref()
620 }
621
622 pub fn save_wrkdir_patterns(&self) -> &[String] {
623 self.file.pkgsrc.save_wrkdir_patterns.as_slice()
624 }
625
626 pub fn tar(&self) -> &PathBuf {
627 &self.file.pkgsrc.tar
628 }
629
630 pub fn build_user(&self) -> Option<&str> {
631 self.file.pkgsrc.build_user.as_deref()
632 }
633
634 pub fn bootstrap(&self) -> Option<&PathBuf> {
635 self.file.pkgsrc.bootstrap.as_ref()
636 }
637
638 /// Get environment variables for a package from the Lua env function/table.
639 pub fn get_pkg_env(
640 &self,
641 idx: &ScanIndex,
642 ) -> Result<std::collections::HashMap<String, String>, String> {
643 self.lua_env.get_env(idx)
644 }
645
646 /// Return environment variables for script execution.
647 pub fn script_env(&self) -> Vec<(String, String)> {
648 let mut envs = vec![
649 ("bob_logdir".to_string(), format!("{}", self.logdir().display())),
650 ("bob_make".to_string(), format!("{}", self.make().display())),
651 (
652 "bob_packages".to_string(),
653 format!("{}", self.packages().display()),
654 ),
655 (
656 "bob_pkgtools".to_string(),
657 format!("{}", self.pkgtools().display()),
658 ),
659 ("bob_pkgsrc".to_string(), format!("{}", self.pkgsrc().display())),
660 ("bob_prefix".to_string(), format!("{}", self.prefix().display())),
661 ("bob_tar".to_string(), format!("{}", self.tar().display())),
662 ];
663 if let Some(build_user) = self.build_user() {
664 envs.push(("bob_build_user".to_string(), build_user.to_string()));
665 }
666 if let Some(bootstrap) = self.bootstrap() {
667 envs.push((
668 "bob_bootstrap".to_string(),
669 format!("{}", bootstrap.display()),
670 ));
671 }
672 envs
673 }
674
675 /// Validate the configuration, checking that required paths and files exist.
676 pub fn validate(&self) -> Result<(), Vec<String>> {
677 let mut errors: Vec<String> = Vec::new();
678
679 // Check pkgsrc directory exists
680 if !self.file.pkgsrc.basedir.exists() {
681 errors.push(format!(
682 "pkgsrc basedir does not exist: {}",
683 self.file.pkgsrc.basedir.display()
684 ));
685 }
686
687 // Check make binary exists (only on host if sandboxes not enabled)
688 // When sandboxes are enabled, the make binary is inside the sandbox
689 if self.file.sandboxes.is_none() && !self.file.pkgsrc.make.exists() {
690 errors.push(format!(
691 "make binary does not exist: {}",
692 self.file.pkgsrc.make.display()
693 ));
694 }
695
696 // Check scripts exist
697 for (name, path) in &self.file.scripts {
698 if !path.exists() {
699 errors.push(format!(
700 "Script '{}' does not exist: {}",
701 name,
702 path.display()
703 ));
704 } else if !path.is_file() {
705 errors.push(format!(
706 "Script '{}' is not a file: {}",
707 name,
708 path.display()
709 ));
710 }
711 }
712
713 // Check sandbox basedir is writable if sandboxes enabled
714 if let Some(sandboxes) = &self.file.sandboxes {
715 // Check parent directory exists or can be created
716 if let Some(parent) = sandboxes.basedir.parent() {
717 if !parent.exists() {
718 errors.push(format!(
719 "Sandbox basedir parent does not exist: {}",
720 parent.display()
721 ));
722 }
723 }
724 }
725
726 // Check logdir can be created
727 if let Some(parent) = self.file.pkgsrc.logdir.parent() {
728 if !parent.exists() {
729 errors.push(format!(
730 "logdir parent directory does not exist: {}",
731 parent.display()
732 ));
733 }
734 }
735
736 // Check packages dir can be created
737 if let Some(parent) = self.file.pkgsrc.packages.parent() {
738 if !parent.exists() {
739 errors.push(format!(
740 "Packages parent directory does not exist: {}",
741 parent.display()
742 ));
743 }
744 }
745
746 if errors.is_empty() { Ok(()) } else { Err(errors) }
747 }
748}
749
750/// Load a Lua configuration file and return a ConfigFile and LuaEnv.
751fn load_lua(filename: &Path) -> Result<(ConfigFile, LuaEnv), String> {
752 let lua = Lua::new();
753
754 // Add config directory to package.path so require() finds relative modules
755 if let Some(config_dir) = filename.parent() {
756 let path_setup = format!(
757 "package.path = '{}' .. '/?.lua;' .. package.path",
758 config_dir.display()
759 );
760 lua.load(&path_setup)
761 .exec()
762 .map_err(|e| format!("Failed to set package.path: {}", e))?;
763 }
764
765 lua.load(filename)
766 .exec()
767 .map_err(|e| format!("Lua execution error: {}", e))?;
768
769 // Get the global table (Lua script should set global variables)
770 let globals = lua.globals();
771
772 // Parse each section
773 let options = parse_options(&globals)
774 .map_err(|e| format!("Error parsing options: {}", e))?;
775 let pkgsrc_table: Table = globals
776 .get("pkgsrc")
777 .map_err(|e| format!("Error getting pkgsrc: {}", e))?;
778 let pkgsrc = parse_pkgsrc(&globals)
779 .map_err(|e| format!("Error parsing pkgsrc: {}", e))?;
780 let scripts = parse_scripts(&globals)
781 .map_err(|e| format!("Error parsing scripts: {}", e))?;
782 let sandboxes = parse_sandboxes(&globals)
783 .map_err(|e| format!("Error parsing sandboxes: {}", e))?;
784
785 // Store env function/table in registry if it exists
786 let env_key = if let Ok(env_value) = pkgsrc_table.get::<Value>("env") {
787 if !env_value.is_nil() {
788 let key = lua.create_registry_value(env_value).map_err(|e| {
789 format!("Failed to store env in registry: {}", e)
790 })?;
791 Some(Arc::new(key))
792 } else {
793 None
794 }
795 } else {
796 None
797 };
798
799 let lua_env = LuaEnv { lua: Arc::new(Mutex::new(lua)), env_key };
800
801 let config = ConfigFile { options, pkgsrc, scripts, sandboxes };
802
803 Ok((config, lua_env))
804}
805
806fn parse_options(globals: &Table) -> LuaResult<Option<Options>> {
807 let options: Value = globals.get("options")?;
808 if options.is_nil() {
809 return Ok(None);
810 }
811
812 let table = options
813 .as_table()
814 .ok_or_else(|| mlua::Error::runtime("options must be a table"))?;
815
816 Ok(Some(Options {
817 build_threads: table.get("build_threads").ok(),
818 scan_threads: table.get("scan_threads").ok(),
819 verbose: table.get("verbose").ok(),
820 }))
821}
822
823fn parse_pkgsrc(globals: &Table) -> LuaResult<Pkgsrc> {
824 let pkgsrc: Table = globals.get("pkgsrc")?;
825
826 let basedir: String = pkgsrc.get("basedir")?;
827 let bootstrap: Option<PathBuf> =
828 pkgsrc.get::<Option<String>>("bootstrap")?.map(PathBuf::from);
829 let build_user: Option<String> =
830 pkgsrc.get::<Option<String>>("build_user")?;
831 let logdir: String = pkgsrc.get("logdir")?;
832 let make: String = pkgsrc.get("make")?;
833 let packages: String = pkgsrc.get("packages")?;
834 let pkgtools: String = pkgsrc.get("pkgtools")?;
835 let prefix: String = pkgsrc.get("prefix")?;
836 let tar: String = pkgsrc.get("tar")?;
837
838 let pkgpaths: Option<Vec<PkgPath>> =
839 match pkgsrc.get::<Value>("pkgpaths")? {
840 Value::Nil => None,
841 Value::Table(t) => {
842 let paths: Vec<PkgPath> = t
843 .sequence_values::<String>()
844 .filter_map(|r| r.ok())
845 .filter_map(|s| PkgPath::new(&s).ok())
846 .collect();
847 if paths.is_empty() { None } else { Some(paths) }
848 }
849 _ => None,
850 };
851
852 let report_dir: Option<PathBuf> =
853 pkgsrc.get::<Option<String>>("report_dir")?.map(PathBuf::from);
854
855 let save_wrkdir_patterns: Vec<String> =
856 match pkgsrc.get::<Value>("save_wrkdir_patterns")? {
857 Value::Nil => Vec::new(),
858 Value::Table(t) => {
859 t.sequence_values::<String>().filter_map(|r| r.ok()).collect()
860 }
861 _ => Vec::new(),
862 };
863
864 Ok(Pkgsrc {
865 basedir: PathBuf::from(basedir),
866 bootstrap,
867 build_user,
868 logdir: PathBuf::from(logdir),
869 make: PathBuf::from(make),
870 packages: PathBuf::from(packages),
871 pkgtools: PathBuf::from(pkgtools),
872 pkgpaths,
873 prefix: PathBuf::from(prefix),
874 report_dir,
875 save_wrkdir_patterns,
876 tar: PathBuf::from(tar),
877 })
878}
879
880fn parse_scripts(globals: &Table) -> LuaResult<HashMap<String, PathBuf>> {
881 let scripts: Value = globals.get("scripts")?;
882 if scripts.is_nil() {
883 return Ok(HashMap::new());
884 }
885
886 let table = scripts
887 .as_table()
888 .ok_or_else(|| mlua::Error::runtime("scripts must be a table"))?;
889
890 let mut result = HashMap::new();
891 for pair in table.pairs::<String, String>() {
892 let (k, v) = pair?;
893 result.insert(k, PathBuf::from(v));
894 }
895
896 Ok(result)
897}
898
899fn parse_sandboxes(globals: &Table) -> LuaResult<Option<Sandboxes>> {
900 let sandboxes: Value = globals.get("sandboxes")?;
901 if sandboxes.is_nil() {
902 return Ok(None);
903 }
904
905 let table = sandboxes
906 .as_table()
907 .ok_or_else(|| mlua::Error::runtime("sandboxes must be a table"))?;
908
909 let basedir: String = table.get("basedir")?;
910
911 let actions_value: Value = table.get("actions")?;
912 let actions = if actions_value.is_nil() {
913 Vec::new()
914 } else {
915 let actions_table = actions_value.as_table().ok_or_else(|| {
916 mlua::Error::runtime("sandboxes.actions must be a table")
917 })?;
918 parse_actions(actions_table)?
919 };
920
921 Ok(Some(Sandboxes { basedir: PathBuf::from(basedir), actions }))
922}
923
924fn parse_actions(table: &Table) -> LuaResult<Vec<Action>> {
925 table.sequence_values::<Table>().map(|v| Action::from_lua(&v?)).collect()
926}