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