1use super::{Task, TaskResult};
8use crate::OutputCapture;
9use crate::config::BackendConfig;
10use crate::environment::Environment;
11use crate::{Error, Result};
12use async_trait::async_trait;
13use std::path::Path;
14use std::process::Stdio;
15use std::sync::Arc;
16use tokio::process::Command;
17
18pub struct TaskExecutionContext<'a> {
21 pub name: &'a str,
23 pub task: &'a Task,
25 pub environment: &'a Environment,
27 pub project_root: &'a Path,
29 pub capture_output: OutputCapture,
31}
32
33#[async_trait]
35pub trait TaskBackend: Send + Sync {
36 async fn execute(&self, ctx: &TaskExecutionContext<'_>) -> Result<TaskResult>;
38
39 fn name(&self) -> &'static str;
41}
42
43pub struct HostBackend;
45
46impl Default for HostBackend {
47 fn default() -> Self {
48 Self
49 }
50}
51
52impl HostBackend {
53 pub fn new() -> Self {
54 Self
55 }
56}
57
58#[async_trait]
59impl TaskBackend for HostBackend {
60 async fn execute(&self, ctx: &TaskExecutionContext<'_>) -> Result<TaskResult> {
61 tracing::info!(
62 task = %ctx.name,
63 backend = "host",
64 "Executing task on host"
65 );
66
67 let command_spec = ctx
68 .task
69 .command_spec(|command| ctx.environment.resolve_command(command))?;
70
71 let mut cmd = Command::new(&command_spec.program);
72 cmd.args(&command_spec.args);
73
74 cmd.current_dir(ctx.project_root);
76
77 cmd.env_clear();
79 for (k, v) in &ctx.environment.vars {
80 cmd.env(k, v);
81 }
82
83 for (key, value) in &ctx.task.env {
85 if let Some(s) = value.as_str() {
86 if let Some(host_var) = super::output_refs::parse_passthrough(s) {
87 if let Ok(host_val) = std::env::var(host_var) {
88 cmd.env(key, host_val);
89 }
90 } else if !s.starts_with("cuenv:ref:") {
91 cmd.env(key, s);
92 }
93 } else if let Some(n) = value.as_i64() {
94 cmd.env(key, n.to_string());
95 } else if let Some(b) = value.as_bool() {
96 cmd.env(key, b.to_string());
97 }
98 }
99
100 if ctx.capture_output.should_capture() {
102 let output = cmd
103 .stdout(Stdio::piped())
104 .stderr(Stdio::piped())
105 .output()
106 .await
107 .map_err(|e| Error::Io {
108 source: e,
109 path: None,
110 operation: format!("spawn task {}", ctx.name),
111 })?;
112
113 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
114 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
115 let exit_code = output.status.code().unwrap_or(-1);
116 let success = output.status.success();
117
118 if !success {
119 tracing::warn!(task = %ctx.name, exit = exit_code, "Task failed");
120 }
121
122 Ok(TaskResult {
123 name: ctx.name.to_string(),
124 exit_code: Some(exit_code),
125 stdout,
126 stderr,
127 success,
128 })
129 } else {
130 let status = cmd
132 .stdout(Stdio::inherit())
133 .stderr(Stdio::inherit())
134 .status()
135 .await
136 .map_err(|e| Error::Io {
137 source: e,
138 path: None,
139 operation: format!("spawn task {}", ctx.name),
140 })?;
141
142 let exit_code = status.code().unwrap_or(-1);
143 let success = status.success();
144
145 if !success {
146 tracing::warn!(task = %ctx.name, exit = exit_code, "Task failed");
147 }
148
149 Ok(TaskResult {
150 name: ctx.name.to_string(),
151 exit_code: Some(exit_code),
152 stdout: String::new(), stderr: String::new(),
154 success,
155 })
156 }
157 }
158
159 fn name(&self) -> &'static str {
160 "host"
161 }
162}
163
164pub type BackendFactory = fn(Option<&BackendConfig>, std::path::PathBuf) -> Arc<dyn TaskBackend>;
166
167pub fn create_backend(
172 config: Option<&BackendConfig>,
173 project_root: std::path::PathBuf,
174 cli_backend: Option<&str>,
175) -> Arc<dyn TaskBackend> {
176 create_backend_with_factory(config, project_root, cli_backend, None)
177}
178
179pub fn create_backend_with_factory(
184 config: Option<&BackendConfig>,
185 project_root: std::path::PathBuf,
186 cli_backend: Option<&str>,
187 dagger_factory: Option<BackendFactory>,
188) -> Arc<dyn TaskBackend> {
189 let backend_type = if let Some(b) = cli_backend {
191 b.to_string()
192 } else if let Some(c) = config {
193 c.backend_type.clone()
194 } else {
195 "host".to_string()
196 };
197
198 match backend_type.as_str() {
199 "dagger" => {
200 if let Some(factory) = dagger_factory {
201 factory(config, project_root)
202 } else {
203 tracing::error!(
204 "Dagger backend requested but not available. \
205 Add cuenv-dagger dependency to enable it. \
206 Falling back to host backend."
207 );
208 Arc::new(HostBackend::new())
209 }
210 }
211 _ => Arc::new(HostBackend::new()),
212 }
213}
214
215pub fn should_use_dagger(config: Option<&BackendConfig>, cli_backend: Option<&str>) -> bool {
217 let backend_type = if let Some(b) = cli_backend {
218 b
219 } else if let Some(c) = config {
220 &c.backend_type
221 } else {
222 "host"
223 };
224
225 backend_type == "dagger"
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_host_backend_new() {
234 let backend = HostBackend::new();
235 assert_eq!(backend.name(), "host");
236 }
237
238 #[test]
239 fn test_host_backend_default() {
240 let backend = HostBackend;
241 assert_eq!(backend.name(), "host");
242 }
243
244 #[test]
245 fn test_host_backend_name() {
246 let backend = HostBackend;
247 assert_eq!(backend.name(), "host");
248 }
249
250 #[test]
251 fn test_should_use_dagger_cli_override_dagger() {
252 assert!(should_use_dagger(None, Some("dagger")));
254 }
255
256 #[test]
257 fn test_should_use_dagger_cli_override_host() {
258 assert!(!should_use_dagger(None, Some("host")));
260 }
261
262 #[test]
263 fn test_should_use_dagger_config_dagger() {
264 let config = BackendConfig {
265 backend_type: "dagger".to_string(),
266 options: None,
267 };
268 assert!(should_use_dagger(Some(&config), None));
269 }
270
271 #[test]
272 fn test_should_use_dagger_config_host() {
273 let config = BackendConfig {
274 backend_type: "host".to_string(),
275 options: None,
276 };
277 assert!(!should_use_dagger(Some(&config), None));
278 }
279
280 #[test]
281 fn test_should_use_dagger_default() {
282 assert!(!should_use_dagger(None, None));
284 }
285
286 #[test]
287 fn test_should_use_dagger_cli_overrides_config() {
288 let config = BackendConfig {
289 backend_type: "dagger".to_string(),
290 options: None,
291 };
292 assert!(!should_use_dagger(Some(&config), Some("host")));
294 }
295
296 #[test]
297 fn test_create_backend_defaults_to_host() {
298 let backend = create_backend(None, std::path::PathBuf::from("."), None);
299 assert_eq!(backend.name(), "host");
300 }
301
302 #[test]
303 fn test_create_backend_with_cli_host() {
304 let backend = create_backend(None, std::path::PathBuf::from("."), Some("host"));
305 assert_eq!(backend.name(), "host");
306 }
307
308 #[test]
309 fn test_create_backend_with_config_host() {
310 let config = BackendConfig {
311 backend_type: "host".to_string(),
312 options: None,
313 };
314 let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
315 assert_eq!(backend.name(), "host");
316 }
317
318 #[test]
319 fn test_create_backend_unknown_type_defaults_to_host() {
320 let config = BackendConfig {
321 backend_type: "unknown".to_string(),
322 options: None,
323 };
324 let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
325 assert_eq!(backend.name(), "host");
327 }
328
329 #[test]
330 fn test_create_backend_dagger_without_factory() {
331 let config = BackendConfig {
332 backend_type: "dagger".to_string(),
333 options: None,
334 };
335 let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
337 assert_eq!(backend.name(), "host");
338 }
339
340 #[test]
341 fn test_create_backend_with_factory_dagger() {
342 fn mock_dagger_factory(
344 _config: Option<&BackendConfig>,
345 _project_root: std::path::PathBuf,
346 ) -> Arc<dyn TaskBackend> {
347 Arc::new(HostBackend::new())
348 }
349
350 let config = BackendConfig {
351 backend_type: "dagger".to_string(),
352 options: None,
353 };
354
355 let backend = create_backend_with_factory(
356 Some(&config),
357 std::path::PathBuf::from("."),
358 None,
359 Some(mock_dagger_factory),
360 );
361 assert_eq!(backend.name(), "host");
363 }
364
365 #[test]
366 fn test_create_backend_with_factory_cli_overrides_to_dagger() {
367 fn mock_dagger_factory(
368 _config: Option<&BackendConfig>,
369 _project_root: std::path::PathBuf,
370 ) -> Arc<dyn TaskBackend> {
371 Arc::new(HostBackend::new())
372 }
373
374 let backend = create_backend_with_factory(
376 None,
377 std::path::PathBuf::from("."),
378 Some("dagger"),
379 Some(mock_dagger_factory),
380 );
381 assert_eq!(backend.name(), "host"); }
383
384 #[test]
385 fn test_create_backend_with_factory_cli_overrides_config() {
386 fn mock_dagger_factory(
387 _config: Option<&BackendConfig>,
388 _project_root: std::path::PathBuf,
389 ) -> Arc<dyn TaskBackend> {
390 Arc::new(HostBackend::new())
391 }
392
393 let config = BackendConfig {
394 backend_type: "dagger".to_string(),
395 options: None,
396 };
397
398 let backend = create_backend_with_factory(
400 Some(&config),
401 std::path::PathBuf::from("."),
402 Some("host"),
403 Some(mock_dagger_factory),
404 );
405 assert_eq!(backend.name(), "host");
406 }
407
408 #[test]
409 fn test_backend_config_debug() {
410 let config = BackendConfig {
411 backend_type: "host".to_string(),
412 options: None,
413 };
414 let debug_str = format!("{:?}", config);
415 assert!(debug_str.contains("host"));
416 }
417}