cuenv_core/tasks/
backend.rs1use super::{Task, TaskResult};
8use crate::config::BackendConfig;
9use crate::environment::Environment;
10use crate::{Error, Result};
11use async_trait::async_trait;
12use std::path::Path;
13use std::process::Stdio;
14use std::sync::Arc;
15use tokio::process::Command;
16
17#[async_trait]
19pub trait TaskBackend: Send + Sync {
20 async fn execute(
22 &self,
23 name: &str,
24 task: &Task,
25 environment: &Environment,
26 project_root: &Path,
27 capture_output: bool,
28 ) -> Result<TaskResult>;
29
30 fn name(&self) -> &'static str;
32}
33
34pub struct HostBackend;
36
37impl Default for HostBackend {
38 fn default() -> Self {
39 Self
40 }
41}
42
43impl HostBackend {
44 pub fn new() -> Self {
45 Self
46 }
47}
48
49#[async_trait]
50impl TaskBackend for HostBackend {
51 async fn execute(
52 &self,
53 name: &str,
54 task: &Task,
55 environment: &Environment,
56 project_root: &Path,
57 capture_output: bool,
58 ) -> Result<TaskResult> {
59 tracing::info!(
60 task = %name,
61 backend = "host",
62 "Executing task on host"
63 );
64
65 let resolved_command = environment.resolve_command(&task.command);
67
68 let mut cmd = if let Some(shell) = &task.shell {
70 let mut c = Command::new(shell.command.as_deref().unwrap_or("bash"));
71 if let Some(flag) = &shell.flag {
72 c.arg(flag);
73 } else {
74 c.arg("-c");
75 }
76 c.arg(&task.command);
78 c
79 } else {
80 let mut c = Command::new(resolved_command);
81 c.args(&task.args);
82 c
83 };
84
85 cmd.current_dir(project_root);
87
88 cmd.env_clear();
90 for (k, v) in &environment.vars {
91 cmd.env(k, v);
92 }
93
94 if capture_output {
96 let output = cmd
97 .stdout(Stdio::piped())
98 .stderr(Stdio::piped())
99 .output()
100 .await
101 .map_err(|e| Error::Io {
102 source: e,
103 path: None,
104 operation: format!("spawn task {}", name),
105 })?;
106
107 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
108 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
109 let exit_code = output.status.code().unwrap_or(-1);
110 let success = output.status.success();
111
112 if !success {
113 tracing::warn!(task = %name, exit = exit_code, "Task failed");
114 }
115
116 Ok(TaskResult {
117 name: name.to_string(),
118 exit_code: Some(exit_code),
119 stdout,
120 stderr,
121 success,
122 })
123 } else {
124 let status = cmd
126 .stdout(Stdio::inherit())
127 .stderr(Stdio::inherit())
128 .status()
129 .await
130 .map_err(|e| Error::Io {
131 source: e,
132 path: None,
133 operation: format!("spawn task {}", name),
134 })?;
135
136 let exit_code = status.code().unwrap_or(-1);
137 let success = status.success();
138
139 if !success {
140 tracing::warn!(task = %name, exit = exit_code, "Task failed");
141 }
142
143 Ok(TaskResult {
144 name: name.to_string(),
145 exit_code: Some(exit_code),
146 stdout: String::new(), stderr: String::new(),
148 success,
149 })
150 }
151 }
152
153 fn name(&self) -> &'static str {
154 "host"
155 }
156}
157
158pub type BackendFactory = fn(Option<&BackendConfig>, std::path::PathBuf) -> Arc<dyn TaskBackend>;
160
161pub fn create_backend(
166 config: Option<&BackendConfig>,
167 project_root: std::path::PathBuf,
168 cli_backend: Option<&str>,
169) -> Arc<dyn TaskBackend> {
170 create_backend_with_factory(config, project_root, cli_backend, None)
171}
172
173pub fn create_backend_with_factory(
178 config: Option<&BackendConfig>,
179 project_root: std::path::PathBuf,
180 cli_backend: Option<&str>,
181 dagger_factory: Option<BackendFactory>,
182) -> Arc<dyn TaskBackend> {
183 let backend_type = if let Some(b) = cli_backend {
185 b.to_string()
186 } else if let Some(c) = config {
187 c.backend_type.clone()
188 } else {
189 "host".to_string()
190 };
191
192 match backend_type.as_str() {
193 "dagger" => {
194 if let Some(factory) = dagger_factory {
195 factory(config, project_root)
196 } else {
197 tracing::error!(
198 "Dagger backend requested but not available. \
199 Add cuenv-dagger dependency to enable it. \
200 Falling back to host backend."
201 );
202 Arc::new(HostBackend::new())
203 }
204 }
205 _ => Arc::new(HostBackend::new()),
206 }
207}
208
209pub fn should_use_dagger(config: Option<&BackendConfig>, cli_backend: Option<&str>) -> bool {
211 let backend_type = if let Some(b) = cli_backend {
212 b
213 } else if let Some(c) = config {
214 &c.backend_type
215 } else {
216 "host"
217 };
218
219 backend_type == "dagger"
220}