bob 0.9.0

A pkgsrc package builder
Documentation
/*
 * Copyright (c) 2026 Jonathan Perkin <jonathan@perkin.org.uk>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Instant;

use anyhow::{Context, Result, bail};
use clap::Subcommand;

use bob::config::{Config, PkgsrcEnv};
use bob::logging;
use bob::sandbox::Sandbox;

#[derive(Debug, Subcommand)]
pub enum SandboxCmd {
    /// Create all sandboxes
    Create,
    /// Destroy all sandboxes
    Destroy,
    /// Create a sandbox and start an interactive shell
    Exec,
    /// List currently created sandboxes
    List,
}

pub fn run(config: &Config, cmd: SandboxCmd) -> Result<()> {
    match cmd {
        SandboxCmd::Create => {
            logging::init_stderr_if_enabled();
            let sandbox = Sandbox::new(config);
            if !sandbox.enabled() {
                bail!("No sandboxes configured");
            }
            sandbox.create_all(config.build_threads())?;
        }
        SandboxCmd::Destroy => {
            logging::init_stderr_if_enabled();
            let sandbox = Sandbox::new(config);
            if !sandbox.enabled() {
                bail!("No sandboxes configured");
            }
            sandbox.destroy_all()?;
        }
        SandboxCmd::Exec => {
            logging::init(config.dbdir(), config.log_level())?;
            exec(config)?;
        }
        SandboxCmd::List => {
            let sandbox = Sandbox::new(config);
            if !sandbox.enabled() {
                bail!("No sandboxes configured");
            }
            sandbox.list_all()?;
        }
    }
    Ok(())
}

fn exec(config: &Config) -> Result<()> {
    let sandbox = Sandbox::new(config);
    if !sandbox.enabled() {
        bail!("No sandboxes configured");
    }
    print!("Creating sandbox...");
    let _ = std::io::stdout().flush();
    let start = Instant::now();
    let id = sandbox.claim_id()?;
    let basic_envs = config.script_env(None);
    let result = (|| -> Result<()> {
        if !sandbox.run_pre_build(Some(id), config, basic_envs)? {
            println!(" failed ({:.1}s)", start.elapsed().as_secs_f32());
            bail!("pre-build script failed");
        }
        println!(" done ({:.1}s)", start.elapsed().as_secs_f32());
        println!("Entering sandbox {}...", sandbox.path(id).display());
        let mut cmd = Command::new("/usr/sbin/chroot");
        cmd.arg(sandbox.path(id)).arg("/bin/sh").arg("-i");
        sandbox.apply_environment(&mut cmd);
        cmd.env("PS1", format!("sandbox:{} $PWD# ", id));
        cmd.stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit());
        let status = cmd.status().context("Failed to run chroot shell")?;
        if !status.success() {
            bail!("Shell exited with {}", status);
        }
        Ok(())
    })();
    let pkgsrc_env = PkgsrcEnv::fetch(config, &sandbox, Some(id)).ok();
    let envs = config.script_env(pkgsrc_env.as_ref());
    match sandbox.run_post_build(Some(id), config, envs) {
        Ok(true) => {}
        Ok(false) => eprintln!("Warning: post-build script failed"),
        Err(e) => eprintln!("Warning: post-build script error: {e}"),
    }
    print!("Destroying sandbox...");
    let _ = std::io::stdout().flush();
    let start = Instant::now();
    sandbox.destroy(id)?;
    println!(" done ({:.1}s)", start.elapsed().as_secs_f32());
    result
}