use super::{Task, TaskResult};
use crate::OutputCapture;
use crate::config::BackendConfig;
use crate::environment::Environment;
use crate::{Error, Result};
use async_trait::async_trait;
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
use tokio::process::Command;
pub struct TaskExecutionContext<'a> {
pub name: &'a str,
pub task: &'a Task,
pub environment: &'a Environment,
pub project_root: &'a Path,
pub capture_output: OutputCapture,
}
#[async_trait]
pub trait TaskBackend: Send + Sync {
async fn execute(&self, ctx: &TaskExecutionContext<'_>) -> Result<TaskResult>;
fn name(&self) -> &'static str;
}
pub struct HostBackend;
impl Default for HostBackend {
fn default() -> Self {
Self
}
}
impl HostBackend {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl TaskBackend for HostBackend {
async fn execute(&self, ctx: &TaskExecutionContext<'_>) -> Result<TaskResult> {
tracing::info!(
task = %ctx.name,
backend = "host",
"Executing task on host"
);
let command_spec = ctx
.task
.command_spec(|command| ctx.environment.resolve_command(command))?;
let mut cmd = Command::new(&command_spec.program);
cmd.args(&command_spec.args);
cmd.current_dir(ctx.project_root);
cmd.env_clear();
for (k, v) in &ctx.environment.vars {
cmd.env(k, v);
}
for (key, value) in &ctx.task.env {
if let Some(s) = value.as_str() {
if let Some(host_var) = super::output_refs::parse_passthrough(s) {
if let Ok(host_val) = std::env::var(host_var) {
cmd.env(key, host_val);
}
} else if !s.starts_with("cuenv:ref:") {
cmd.env(key, s);
}
} else if let Some(n) = value.as_i64() {
cmd.env(key, n.to_string());
} else if let Some(b) = value.as_bool() {
cmd.env(key, b.to_string());
}
}
if ctx.capture_output.should_capture() {
let output = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.map_err(|e| Error::Io {
source: e,
path: None,
operation: format!("spawn task {}", ctx.name),
})?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
let success = output.status.success();
if !success {
tracing::warn!(task = %ctx.name, exit = exit_code, "Task failed");
}
Ok(TaskResult {
name: ctx.name.to_string(),
exit_code: Some(exit_code),
stdout,
stderr,
success,
})
} else {
let status = cmd
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await
.map_err(|e| Error::Io {
source: e,
path: None,
operation: format!("spawn task {}", ctx.name),
})?;
let exit_code = status.code().unwrap_or(-1);
let success = status.success();
if !success {
tracing::warn!(task = %ctx.name, exit = exit_code, "Task failed");
}
Ok(TaskResult {
name: ctx.name.to_string(),
exit_code: Some(exit_code),
stdout: String::new(), stderr: String::new(),
success,
})
}
}
fn name(&self) -> &'static str {
"host"
}
}
pub type BackendFactory = fn(Option<&BackendConfig>, std::path::PathBuf) -> Arc<dyn TaskBackend>;
pub fn create_backend(
config: Option<&BackendConfig>,
project_root: std::path::PathBuf,
cli_backend: Option<&str>,
) -> Arc<dyn TaskBackend> {
create_backend_with_factory(config, project_root, cli_backend, None)
}
pub fn create_backend_with_factory(
config: Option<&BackendConfig>,
project_root: std::path::PathBuf,
cli_backend: Option<&str>,
dagger_factory: Option<BackendFactory>,
) -> Arc<dyn TaskBackend> {
let backend_type = if let Some(b) = cli_backend {
b.to_string()
} else if let Some(c) = config {
c.backend_type.clone()
} else {
"host".to_string()
};
match backend_type.as_str() {
"dagger" => {
if let Some(factory) = dagger_factory {
factory(config, project_root)
} else {
tracing::error!(
"Dagger backend requested but not available. \
Add cuenv-dagger dependency to enable it. \
Falling back to host backend."
);
Arc::new(HostBackend::new())
}
}
_ => Arc::new(HostBackend::new()),
}
}
pub fn should_use_dagger(config: Option<&BackendConfig>, cli_backend: Option<&str>) -> bool {
let backend_type = if let Some(b) = cli_backend {
b
} else if let Some(c) = config {
&c.backend_type
} else {
"host"
};
backend_type == "dagger"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_host_backend_new() {
let backend = HostBackend::new();
assert_eq!(backend.name(), "host");
}
#[test]
fn test_host_backend_default() {
let backend = HostBackend;
assert_eq!(backend.name(), "host");
}
#[test]
fn test_host_backend_name() {
let backend = HostBackend;
assert_eq!(backend.name(), "host");
}
#[test]
fn test_should_use_dagger_cli_override_dagger() {
assert!(should_use_dagger(None, Some("dagger")));
}
#[test]
fn test_should_use_dagger_cli_override_host() {
assert!(!should_use_dagger(None, Some("host")));
}
#[test]
fn test_should_use_dagger_config_dagger() {
let config = BackendConfig {
backend_type: "dagger".to_string(),
options: None,
};
assert!(should_use_dagger(Some(&config), None));
}
#[test]
fn test_should_use_dagger_config_host() {
let config = BackendConfig {
backend_type: "host".to_string(),
options: None,
};
assert!(!should_use_dagger(Some(&config), None));
}
#[test]
fn test_should_use_dagger_default() {
assert!(!should_use_dagger(None, None));
}
#[test]
fn test_should_use_dagger_cli_overrides_config() {
let config = BackendConfig {
backend_type: "dagger".to_string(),
options: None,
};
assert!(!should_use_dagger(Some(&config), Some("host")));
}
#[test]
fn test_create_backend_defaults_to_host() {
let backend = create_backend(None, std::path::PathBuf::from("."), None);
assert_eq!(backend.name(), "host");
}
#[test]
fn test_create_backend_with_cli_host() {
let backend = create_backend(None, std::path::PathBuf::from("."), Some("host"));
assert_eq!(backend.name(), "host");
}
#[test]
fn test_create_backend_with_config_host() {
let config = BackendConfig {
backend_type: "host".to_string(),
options: None,
};
let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
assert_eq!(backend.name(), "host");
}
#[test]
fn test_create_backend_unknown_type_defaults_to_host() {
let config = BackendConfig {
backend_type: "unknown".to_string(),
options: None,
};
let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
assert_eq!(backend.name(), "host");
}
#[test]
fn test_create_backend_dagger_without_factory() {
let config = BackendConfig {
backend_type: "dagger".to_string(),
options: None,
};
let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
assert_eq!(backend.name(), "host");
}
#[test]
fn test_create_backend_with_factory_dagger() {
fn mock_dagger_factory(
_config: Option<&BackendConfig>,
_project_root: std::path::PathBuf,
) -> Arc<dyn TaskBackend> {
Arc::new(HostBackend::new())
}
let config = BackendConfig {
backend_type: "dagger".to_string(),
options: None,
};
let backend = create_backend_with_factory(
Some(&config),
std::path::PathBuf::from("."),
None,
Some(mock_dagger_factory),
);
assert_eq!(backend.name(), "host");
}
#[test]
fn test_create_backend_with_factory_cli_overrides_to_dagger() {
fn mock_dagger_factory(
_config: Option<&BackendConfig>,
_project_root: std::path::PathBuf,
) -> Arc<dyn TaskBackend> {
Arc::new(HostBackend::new())
}
let backend = create_backend_with_factory(
None,
std::path::PathBuf::from("."),
Some("dagger"),
Some(mock_dagger_factory),
);
assert_eq!(backend.name(), "host"); }
#[test]
fn test_create_backend_with_factory_cli_overrides_config() {
fn mock_dagger_factory(
_config: Option<&BackendConfig>,
_project_root: std::path::PathBuf,
) -> Arc<dyn TaskBackend> {
Arc::new(HostBackend::new())
}
let config = BackendConfig {
backend_type: "dagger".to_string(),
options: None,
};
let backend = create_backend_with_factory(
Some(&config),
std::path::PathBuf::from("."),
Some("host"),
Some(mock_dagger_factory),
);
assert_eq!(backend.name(), "host");
}
#[test]
fn test_backend_config_debug() {
let config = BackendConfig {
backend_type: "host".to_string(),
options: None,
};
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("host"));
}
}