use anyhow::{Context, Result, bail};
use std::path::Path;
use std::process::Command;
use crate::cmd::run::RunArgs;
use crate::store::Store;
use crate::types::{EventKind, TaskEvent, TaskId};
pub(crate) fn resolve(args: &mut RunArgs) -> Result<()> {
resolve_in(args, crate::agent::env::which_exists("rbox"), &mut Command::new("rbox"))
}
fn resolve_in(args: &mut RunArgs, rbox_present: bool, command: &mut Command) -> Result<()> {
let Some(requested) = args.remote_build.as_deref() else { return Ok(()) };
if args.sandbox || args.container.is_some() {
bail!("--remote-build conflicts with --sandbox and --container: rbox needs host Tailscale");
}
if !rbox_present {
bail!("Remote build requires rbox on PATH; rbox CLI not found");
}
args.remote_build = Some(resolve_with(requested, command)?);
Ok(())
}
pub(crate) fn project_default(value: Option<&str>) -> Option<String> {
project_default_with(value, crate::agent::env::which_exists("rbox"))
}
fn project_default_with(value: Option<&str>, rbox_present: bool) -> Option<String> {
match value {
Some("auto") if !rbox_present => {
aid_warn!("[aid] project remote_build=auto ignored: rbox not on PATH");
None
}
other => other.map(str::to_string),
}
}
fn resolve_with(requested: &str, command: &mut Command) -> Result<String> {
if requested != "auto" { return Ok(requested.to_string()); }
let output = command.args(["pick", "--role", "rust-build"]).output()
.context("Failed to run rbox pick --role rust-build")?;
if !output.status.success() {
bail!("Remote build box selection failed: {}", String::from_utf8_lossy(&output.stderr).trim());
}
let name = String::from_utf8(output.stdout).context("rbox pick returned invalid UTF-8")?;
let name = name.trim();
if name.is_empty() { bail!("rbox pick returned an empty build box name"); }
Ok(name.to_string())
}
pub(crate) fn saved_box(store: &Store, task_id: &str) -> Result<Option<String>> {
Ok(RunArgs::saved_for_task(store, task_id)?.and_then(|args| args.remote_build))
}
pub(crate) fn record(store: &Store, task_id: &TaskId, args: &RunArgs) -> Result<()> {
let Some(name) = args.remote_build.as_deref() else { return Ok(()) };
store.insert_event(&TaskEvent {
task_id: task_id.clone(), timestamp: chrono::Local::now(), event_kind: EventKind::Milestone,
detail: format!("Remote build box: {name}"),
metadata: Some(serde_json::json!({"remote_build": name})),
})?;
Ok(())
}
pub(crate) fn append_prompt(prompt: &mut String, name: Option<&str>) {
if let Some(name) = name {
prompt.push_str(&format!("\n\nCargo build/check/test run on remote box {name} through a PATH shim. Expect sync and lock latency; do not bypass the shim. `cargo fmt` stays local."));
}
}
pub(crate) fn configure_task(
store: &Store, task_id: &str, cmd: &mut Command, home: &Path,
) -> Result<()> {
if let Some(name) = saved_box(store, task_id)? {
configure(cmd, home, &name)?;
}
Ok(())
}
fn configure(cmd: &mut Command, home: &Path, name: &str) -> Result<()> {
let shim_dir = home.join(".aid-build-shims");
if std::fs::symlink_metadata(&shim_dir).is_ok_and(|m| m.file_type().is_symlink()) {
bail!("Remote Cargo shim directory must not be a symlink: {}", shim_dir.display());
}
std::fs::create_dir_all(&shim_dir).context("Failed to create remote Cargo shim directory")?;
let shim = shim_dir.join("cargo");
std::fs::write(&shim, include_str!("remote_build/cargo.sh"))?;
#[cfg(unix)] {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755))?;
}
let path = cmd.get_envs().find(|(key, _)| *key == "PATH")
.and_then(|(_, value)| value.map(ToOwned::to_owned))
.or_else(|| std::env::var_os("PATH")).unwrap_or_default();
let paths = std::iter::once(shim_dir).chain(std::env::split_paths(&path));
cmd.env("PATH", std::env::join_paths(paths)?).env("AID_BUILD_BOX", name);
Ok(())
}
pub(crate) fn verify(
store: &Store, task_id: &str, path: &Path, command: Option<&str>,
target: Option<&str>, container: Option<&str>,
) -> Result<crate::verify::VerifyResult> {
let Some(name) = saved_box(store, task_id)? else {
return crate::verify_cargo::run_verify_with_store(store, path, command, target, container);
};
let mut environment = Command::new("cargo");
configure(&mut environment, &crate::paths::task_dir(task_id).join("home"), &name)?;
let env = environment.get_envs().filter_map(|(key, value)| {
value.map(|value| (key.to_string_lossy().into_owned(), value.to_string_lossy().into_owned()))
}).collect::<Vec<_>>();
crate::verify::run_verify_with_env(
path, command, target, container, crate::verify::VERIFY_TIMEOUT, &env,
)
}
#[cfg(test)]
#[path = "remote_build/tests.rs"]
mod tests;
#[cfg(test)]
#[path = "remote_build/shim_tests.rs"]
mod shim_tests;