use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use crate::types::AgentKind;
use super::RunOpts;
pub(crate) use super::cargo_target::BranchTargetSeedOutcome;
#[path = "cargo_target_layout.rs"]
mod cargo_target_layout;
use cargo_target_layout::CargoTargetLayout;
#[derive(Debug)]
pub(crate) struct CliCommandOutput {
pub success: bool,
pub stdout: String,
pub stderr: String,
}
pub(crate) type CliCommandRunner<'a> = dyn Fn(&str, &[&str]) -> Result<CliCommandOutput> + 'a;
const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR";
const CARGO_MANIFEST_NAME: &str = "Cargo.toml";
const BASE_TARGET_DIR_NAME: &str = "_base";
const SHARED_TARGET_DIR_NAME: &str = "cargo-target";
pub fn agent_has_fs_access(_kind: &AgentKind) -> bool {
true }
pub fn shared_target_dir() -> Option<String> {
Some(target_layout(None)?.source.to_string_lossy().into_owned())
}
fn target_layout(project_dir: Option<&str>) -> Option<CargoTargetLayout> {
let branch_root = std::env::var_os(CARGO_TARGET_DIR_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| crate::paths::aid_dir().join(SHARED_TARGET_DIR_NAME));
Some(cargo_target_layout::layout_for_project(branch_root, project_dir))
}
pub fn target_dir_for_worktree(worktree_branch: Option<&str>) -> Option<String> {
target_dir_for_project(None, worktree_branch)
}
pub(crate) fn target_dir_for_project(
project_dir: Option<&str>,
worktree_branch: Option<&str>,
) -> Option<String> {
let layout = target_layout(project_dir)?;
match worktree_branch {
Some(branch) => {
let target = target_dir_for_branch(&layout.branch_root, branch);
Some(target.to_string_lossy().into_owned())
}
None => Some(layout.source.to_string_lossy().into_owned()),
}
}
pub(crate) fn branch_target_root() -> Option<PathBuf> {
Some(target_layout(None)?.branch_root)
}
pub(crate) fn branch_target_name(branch: &str) -> String {
branch.replace('/', "-")
}
pub(crate) fn seed_branch_target_dir(
project_dir: Option<&str>,
worktree_branch: &str,
) -> Option<BranchTargetSeedOutcome> {
let layout = target_layout(project_dir)?;
let target = target_dir_for_branch(&layout.branch_root, worktree_branch);
let outcome = if layout.source.is_dir() {
super::cargo_target::seed_branch_target_from_source(&layout.source, &target)
} else {
super::cargo_target::seed_branch_target_from_root_entries(&layout.branch_root, &target)
};
let outcome = ensure_branch_target_dir(&target, outcome);
Some(outcome)
}
fn ensure_branch_target_dir(
target: &Path,
outcome: BranchTargetSeedOutcome,
) -> BranchTargetSeedOutcome {
if target.is_dir() {
return outcome;
}
let target_display = target.to_string_lossy().into_owned();
match std::fs::create_dir_all(target) {
Ok(()) => outcome,
Err(err) => BranchTargetSeedOutcome::Skipped {
target: target_display,
reason: format!("seed did not create target directory: {err}"),
},
}
}
pub(crate) fn remove_branch_target_dir_if_unused(repo_dir: &Path, branch: &str) -> anyhow::Result<bool> {
let Some(layout) = target_layout(repo_dir.to_str()) else {
return Ok(false);
};
let target = target_dir_for_branch(&layout.branch_root, branch);
if target.file_name().and_then(|name| name.to_str()) == Some(BASE_TARGET_DIR_NAME) {
return Ok(false);
}
if branch_has_live_worktree(repo_dir, branch)? {
return Ok(false);
}
super::cargo_target::remove_branch_target_dir(&target)
}
pub fn apply_rust_build_cache_env(
cmd: &mut Command,
project_dir: Option<&str>,
worktree_branch: Option<&str>,
) {
if let Some(target_dir) = rust_build_cache_target_dir(project_dir, worktree_branch) {
apply_cargo_target_env(cmd, Some(target_dir.as_str()));
}
}
pub fn rust_build_cache_target_dir(
project_dir: Option<&str>,
worktree_branch: Option<&str>,
) -> Option<String> {
if !is_rust_project(project_dir) {
return None;
}
target_dir_for_project(project_dir, worktree_branch)
}
pub fn apply_cargo_target_env(cmd: &mut Command, cargo_target_dir: Option<&str>) {
if let Some(target_dir) = cargo_target_dir {
cmd.env(CARGO_TARGET_DIR_ENV, target_dir);
}
}
pub fn apply_codex_home_env(cmd: &mut Command) -> anyhow::Result<()> {
let codex_home = super::home_isolation::resolve_real_home()?.join(".codex");
cmd.env("CODEX_HOME", codex_home);
Ok(())
}
pub(crate) fn should_use_durable_codex_home(
agent_kind: AgentKind,
sandbox: bool,
containerized: bool,
) -> bool {
agent_kind == AgentKind::Codex && !sandbox && !containerized
}
pub fn cargo_target_env_arg(cargo_target_dir: &str) -> String {
format!("{CARGO_TARGET_DIR_ENV}={cargo_target_dir}")
}
fn target_dir_for_branch(base: &Path, branch: &str) -> PathBuf {
base.join(branch_target_name(branch))
}
fn branch_has_live_worktree(repo_dir: &Path, branch: &str) -> anyhow::Result<bool> {
let output = Command::new("git")
.args(["-C", &repo_dir.to_string_lossy(), "worktree", "list", "--porcelain"])
.output()?;
if !output.status.success() {
return Ok(true);
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(worktree_list_has_branch(&stdout, branch))
}
fn worktree_list_has_branch(output: &str, branch: &str) -> bool {
output.split("\n\n").any(|block| {
!block.lines().any(|line| line.starts_with("prunable "))
&& block.lines().any(|line| {
line.strip_prefix("branch refs/heads/") == Some(branch)
})
})
}
pub fn is_rust_project(dir: Option<&str>) -> bool {
resolve_project_dir(dir).join(CARGO_MANIFEST_NAME).is_file()
}
fn resolve_project_dir(dir: Option<&str>) -> PathBuf {
dir.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."))
}
pub fn set_git_ceiling(cmd: &mut Command, dir: &str) {
let path = std::path::Path::new(dir);
if let Some(parent) = path.parent() {
cmd.env("GIT_CEILING_DIRECTORIES", parent);
}
}
pub fn apply_run_env(
cmd: &mut Command,
opts: &RunOpts,
task_id: Option<&str>,
) -> anyhow::Result<super::home_isolation::IsolatedHomeGuard> {
cmd.env("AID_HOME", crate::paths::aid_dir());
if let Some(env) = opts.env.as_ref() {
for (key, value) in env {
cmd.env(key, value);
}
}
if let Some(env_forward) = opts.env_forward.as_ref() {
for name in env_forward {
if let Ok(value) = std::env::var(name) {
cmd.env(name, value);
}
}
}
let guard = super::home_isolation::IsolatedHomeGuard::create(task_id)?;
guard.apply_toolchain_env(cmd);
cmd.env("HOME", guard.path());
Ok(guard)
}
pub(crate) fn which_exists(name: &str) -> bool {
if let Some(marker) = super::env_identity::identity_marker(name) {
return super::env_identity::binary_identity_matches(name, marker);
}
let paths = std::env::var_os("PATH")
.map(|value| std::env::split_paths(&value).collect::<Vec<_>>())
.unwrap_or_default();
executable_in_paths(name, paths)
}
pub(crate) fn installed_agents(candidates: &[(&str, AgentKind)]) -> Vec<AgentKind> {
let results = std::thread::scope(|scope| {
let probes =
candidates
.iter()
.map(|(name, kind)| scope.spawn(move || (*kind, which_exists(name))))
.collect::<Vec<_>>();
probes.into_iter().filter_map(|probe| probe.join().ok()).collect::<Vec<_>>()
});
let mut found = Vec::new();
for (kind, available) in results {
if available && !found.contains(&kind) {
found.push(kind);
}
}
found
}
fn executable_in_paths<I>(name: &str, paths: I) -> bool
where
I: IntoIterator<Item = PathBuf>,
{
let candidate = Path::new(name);
if candidate.components().count() > 1 {
return is_executable_file(candidate);
}
paths
.into_iter()
.map(|path| path.join(name))
.any(|path| is_executable_file(&path))
}
fn is_executable_file(path: &Path) -> bool {
let Ok(metadata) = fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
#[cfg(test)]
#[path = "env_tests.rs"]
mod tests;