feat(sbx): install Docker/Multipass on demand from /sbx launch
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
Previously, launching a sandbox on a machine without the backend binary died with a raw "not installed" error and no path forward. Now the missing backend can be installed in-line, gated on explicit consent, so a fresh checkout can go from zero to a running sandbox without leaving the TUI. Folded into the existing `/sbx launch` verb rather than a new command (menu is already dense): `/sbx launch docker|multipass [image] install`. The `install` token opts in; without it the user gets an actionable error naming the exact retry. The token is filtered out of positional image parsing so it never shadows a custom image. The install runs off-thread inside the existing spawn_launch task, before provisioning, so the TUI never blocks and the *launching guard is cleared via BrokerMsg on failure. A fresh Docker install also leaves its daemon up, so launch proceeds straight through. Scripts (detect-first, never silent, --plan dry-run, idempotent if already present): - ensure-multipass.sh (new): Linux→snap (clear failure if snapd absent), macOS→brew cask, Windows→winget. - ensure-docker.sh: new --install mode using Docker's OFFICIAL, GPG-verified apt repo (docker-ce) on Debian/Ubuntu, with dnf (Fedora/RHEL) and pacman (Arch) fallbacks. Deliberately avoids piping get.docker.com into a root shell. Existing daemon-start path intact. sbx.rs: docker_installed()/multipass_installed() detectors and ensure_docker_install()/ensure_multipass_install() wrappers; ENSURE_MULTIPASS const. ui.rs: help text documents the [install] option on both backends. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ use std::sync::mpsc;
|
||||
|
||||
/// Helper that ensures the Docker daemon is running (ships in hh/scripts/).
|
||||
const ENSURE_DOCKER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-docker.sh");
|
||||
/// Detect-first Multipass installer (ships in hh/scripts/).
|
||||
const ENSURE_MULTIPASS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-multipass.sh");
|
||||
/// Detect-first VirtualBox installer (ships in hh/scripts/).
|
||||
const ENSURE_VBOX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-vbox.sh");
|
||||
/// Baseline dev-toolchain installer run inside Docker sandboxes (hh/scripts/).
|
||||
@@ -23,6 +25,19 @@ const SBX_TOOLS_JSON: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/sandb
|
||||
/// Creates a fresh, cloud-init-provisioned Ubuntu VirtualBox VM (hh/scripts/).
|
||||
const VBOX_NEW: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/vbox-new.sh");
|
||||
|
||||
/// Is the `docker` binary installed? (`docker --version` succeeds.) This is a
|
||||
/// weaker check than `docker_daemon_up`: the CLI can be present while the daemon
|
||||
/// is down. We need both before a Docker sandbox can launch.
|
||||
pub fn docker_installed() -> bool {
|
||||
Command::new("docker")
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Is the Docker daemon accepting connections? (`docker info` succeeds.)
|
||||
pub fn docker_daemon_up() -> bool {
|
||||
Command::new("docker")
|
||||
@@ -53,6 +68,54 @@ fn start_docker_daemon() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install Docker via `ensure-docker.sh --install --yes` (Docker's official,
|
||||
/// GPG-verified repo), then leave the daemon started. Consent is the caller's
|
||||
/// job (they passed `install`); the script is idempotent if Docker is present.
|
||||
/// Returns the script's last error line on failure (e.g. needs sudo).
|
||||
pub fn ensure_docker_install() -> Result<()> {
|
||||
let out = Command::new("bash")
|
||||
.arg(ENSURE_DOCKER)
|
||||
.arg("--install")
|
||||
.arg("--yes")
|
||||
.output()
|
||||
.context("running ensure-docker.sh --install")?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
let last = err.lines().last().unwrap_or("could not install Docker");
|
||||
anyhow::bail!("{last}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Is Multipass installed? (`multipass version` succeeds.)
|
||||
pub fn multipass_installed() -> bool {
|
||||
Command::new("multipass")
|
||||
.arg("version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Install Multipass via `ensure-multipass.sh --yes`. On Linux this is a snap
|
||||
/// install (the only supported channel). Consent is the caller's job; the
|
||||
/// script is idempotent if Multipass is already present. Returns the script's
|
||||
/// last error line on failure (e.g. snapd missing, or needs sudo).
|
||||
pub fn ensure_multipass_install() -> Result<()> {
|
||||
let out = Command::new("bash")
|
||||
.arg(ENSURE_MULTIPASS)
|
||||
.arg("--yes")
|
||||
.output()
|
||||
.context("running ensure-multipass.sh")?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
let last = err.lines().last().unwrap_or("could not install Multipass");
|
||||
anyhow::bail!("{last}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- VirtualBox (local GUI VMs) ---------------------------------------------
|
||||
// VirtualBox is integrated as a *local* facility rather than a shared-PTY
|
||||
// backend: a room shares a VM by handing out its appliance, and each member
|
||||
|
||||
Reference in New Issue
Block a user