use std::fmt::Write as _;
use std::fs;
use std::process::{Command, Stdio};
use std::time::Instant;
use anyhow::{Context, Result, bail};
use clap::{ArgGroup, Args, Subcommand};
use bob::config::{Config, Pkgsrc, PkgsrcEnv};
use bob::logging;
use bob::sandbox::Sandbox;
#[derive(Debug, Subcommand)]
pub enum SandboxCmd {
Create(CreateArgs),
Destroy(DestroyArgs),
Exec,
List,
}
#[derive(Debug, Args)]
pub struct CreateArgs {
#[arg(short = 'n', long, value_name = "N", default_value_t = 1)]
pub count: usize,
}
#[derive(Debug, Args)]
#[command(group = ArgGroup::new("target").required(true).multiple(false))]
pub struct DestroyArgs {
#[arg(short = 'a', long, group = "target")]
pub all: bool,
#[arg(value_name = "ID", group = "target", num_args = 1..)]
pub ids: Vec<usize>,
}
pub fn run(config: &Config, pkgsrc: Option<&Pkgsrc>, cmd: SandboxCmd) -> Result<()> {
match cmd {
SandboxCmd::Create(args) => {
logging::init_stderr_if_enabled();
if args.count == 0 {
bail!("--count must be at least 1");
}
let sandbox = Sandbox::new(config, pkgsrc);
if !sandbox.enabled() {
bail!("No sandboxes configured");
}
let ids = sandbox.create_all(args.count)?;
for id in ids {
if !bob::try_println(&format!("{}", sandbox.path(id).display())) {
break;
}
}
}
SandboxCmd::Destroy(args) => {
logging::init_stderr_if_enabled();
let sandbox = Sandbox::new(config, pkgsrc);
if !sandbox.enabled() {
bail!("No sandboxes configured");
}
if pkgsrc.is_some() {
match bob::Database::open(config.dbdir()).and_then(|db| db.load_pkgsrc_env()) {
Ok(env) => sandbox.set_pkgsrc_env(env),
Err(_) => eprintln!(
"Warning: No database available, unable to remove pkgsrc directories."
),
}
}
if args.all {
sandbox.destroy_all()?;
} else {
sandbox.destroy_ids(&args.ids)?;
}
}
SandboxCmd::Exec => {
logging::init(config.dbdir(), config.log_level())?;
exec(config, pkgsrc)?;
}
SandboxCmd::List => {
let sandbox = Sandbox::new(config, pkgsrc);
if !sandbox.enabled() {
bail!("No sandboxes configured");
}
sandbox.list_all()?;
}
}
Ok(())
}
fn exec(config: &Config, pkgsrc: Option<&Pkgsrc>) -> Result<()> {
let sandbox = Sandbox::new_dev(config, pkgsrc);
if !sandbox.enabled() {
bail!("No sandboxes configured");
}
bob::print_status("Creating sandbox");
let start = Instant::now();
let id = match sandbox.claim_id() {
Ok(id) => id,
Err(e) => {
bob::print_failed("Creating sandbox", start.elapsed());
return Err(e);
}
};
let result = (|| -> Result<()> {
match sandbox.run_pre_build(Some(id)) {
Ok(()) => bob::print_elapsed("Creating sandbox", start.elapsed()),
Err(e) => {
bob::print_failed("Creating sandbox", start.elapsed());
return Err(e.context("pre-build failed"));
}
}
if let Some(pkgsrc) = pkgsrc {
let pkgsrc_env = PkgsrcEnv::fetch(pkgsrc, &sandbox, Some(id))?;
sandbox.set_pkgsrc_env(pkgsrc_env);
}
let init_path = write_shell_init(config, &sandbox, id)?;
println!("Entering sandbox {}...", sandbox.path(id).display());
let mut cmd = Command::new("/usr/sbin/chroot");
cmd.arg(sandbox.path(id)).arg("/bin/sh").arg(&init_path);
sandbox.apply_dev_environment(&mut cmd);
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(())
})();
if let Err(e) = sandbox.run_post_build(Some(id)) {
eprintln!("Warning: post-build error: {e:#}");
}
bob::print_status("Destroying sandbox");
let start = Instant::now();
sandbox.destroy(id)?;
bob::print_elapsed("Destroying sandbox", start.elapsed());
result
}
fn write_shell_init(config: &Config, sandbox: &Sandbox, id: usize) -> Result<String> {
let dev_ctx = config.environment().and_then(|e| e.dev.as_ref());
let interactive_shell = dev_ctx
.and_then(|c| c.shell.as_ref())
.map(|p| p.display().to_string())
.unwrap_or_else(|| "/bin/sh".to_string());
let mut script = String::new();
script.push_str("#!/bin/sh\n");
let mut bob_vars = sandbox.script_env();
bob_vars.push(("bob_sandbox_id".to_string(), id.to_string()));
bob_vars.sort_by(|a, b| a.0.cmp(&b.0));
for (name, value) in &bob_vars {
let _ = writeln!(script, "export {}={}", name, bob_dquote(value));
}
if let Some(ctx) = dev_ctx {
let mut dev_vars: Vec<(&String, &String)> = ctx.vars.iter().collect();
dev_vars.sort_by(|a, b| a.0.cmp(b.0));
for (name, value) in dev_vars {
let _ = writeln!(script, "export {}={}", name, value);
}
}
script.push_str("rm -f /.bob/shell-init\n");
let _ = writeln!(script, "exec {} -i", interactive_shell);
let host_path = sandbox.path(id).join(".bob/shell-init");
fs::write(&host_path, &script)
.with_context(|| format!("Failed to write {}", host_path.display()))?;
Ok("/.bob/shell-init".to_string())
}
fn bob_dquote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
if matches!(c, '"' | '\\' | '`' | '$') {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}