1use crate::{
2 error::{ErrorData, Result},
3 traits::{Binding, Build},
4};
5use alien_core::{
6 bindings::{BuildBinding, LocalBuildBinding},
7 BuildConfig, BuildExecution, BuildStatus,
8};
9use alien_error::{AlienError, Context, IntoAlienError};
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::{
13 process::Stdio,
14 time::{SystemTime, UNIX_EPOCH},
15};
16use tokio::process::Command;
17use uuid::Uuid;
18
19#[derive(Debug, Serialize, Deserialize)]
20struct BuildMetadata {
21 uuid: String,
22 pid: u32,
23 start_time: String,
24 end_time: Option<String>,
25 status: BuildStatus,
26}
27
28#[derive(Debug)]
32pub struct LocalBuild {
33 binding_name: String,
34 base_dir: std::path::PathBuf,
35 build_env_vars: std::collections::HashMap<String, String>,
36}
37
38impl LocalBuild {
39 pub fn new(binding_name: String, binding: alien_core::bindings::BuildBinding) -> Result<Self> {
41 let config = match binding {
43 BuildBinding::Local(config) => config,
44 _ => {
45 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
46 binding_name: binding_name.clone(),
47 reason: "Expected Local binding, got different service type".to_string(),
48 }));
49 }
50 };
51
52 let data_dir = config
53 .data_dir
54 .into_value(&binding_name, "data_dir")
55 .context(ErrorData::BindingConfigInvalid {
56 binding_name: binding_name.clone(),
57 reason: "Failed to extract data_dir from binding".to_string(),
58 })?;
59
60 let build_env_vars = config
61 .build_env_vars
62 .into_value(&binding_name, "build_env_vars")
63 .context(ErrorData::BindingConfigInvalid {
64 binding_name: binding_name.clone(),
65 reason: "Failed to extract build_env_vars from binding".to_string(),
66 })?;
67
68 let base_dir = std::path::PathBuf::from(data_dir).join(&binding_name);
69
70 std::fs::create_dir_all(&base_dir)
72 .into_alien_error()
73 .context(ErrorData::BindingConfigInvalid {
74 binding_name: binding_name.clone(),
75 reason: "Failed to create build directory".to_string(),
76 })?;
77
78 Ok(Self {
79 binding_name,
80 base_dir,
81 build_env_vars,
82 })
83 }
84
85 pub fn new_from_path(binding_name: String, base_dir: std::path::PathBuf) -> Self {
87 Self {
88 binding_name,
89 base_dir,
90 build_env_vars: std::collections::HashMap::new(),
91 }
92 }
93
94 fn encode_build_id(uuid: &str, pid: u32, timestamp: u64) -> String {
96 format!("{}_{}_{}", uuid, pid, timestamp)
97 }
98
99 fn decode_build_id(build_id: &str) -> Result<(String, u32, u64)> {
101 let parts: Vec<&str> = build_id.split('_').collect();
102 if parts.len() != 3 {
103 return Err(AlienError::new(ErrorData::BuildOperationFailed {
104 binding_name: "local".to_string(),
105 operation: format!("invalid build ID format: {}", build_id),
106 }));
107 }
108
109 let uuid = parts[0].to_string();
110 let pid = parts[1].parse::<u32>().map_err(|_| {
111 AlienError::new(ErrorData::BuildOperationFailed {
112 binding_name: "local".to_string(),
113 operation: format!("invalid PID in build ID: {}", build_id),
114 })
115 })?;
116 let timestamp = parts[2].parse::<u64>().map_err(|_| {
117 AlienError::new(ErrorData::BuildOperationFailed {
118 binding_name: "local".to_string(),
119 operation: format!("invalid timestamp in build ID: {}", build_id),
120 })
121 })?;
122
123 Ok((uuid, pid, timestamp))
124 }
125
126 fn create_build_dir(&self, uuid: &str) -> Result<std::path::PathBuf> {
128 let build_dir = self.base_dir.join("builds").join(uuid);
129 std::fs::create_dir_all(&build_dir)
130 .into_alien_error()
131 .context(ErrorData::BuildOperationFailed {
132 binding_name: self.binding_name.clone(),
133 operation: "create build directory".to_string(),
134 })?;
135 Ok(build_dir)
136 }
137
138 fn save_build_metadata(&self, uuid: &str, metadata: &BuildMetadata) -> Result<()> {
140 let build_dir = self.base_dir.join("builds").join(uuid);
141 let metadata_path = build_dir.join("metadata.json");
142
143 let metadata_json = serde_json::to_string_pretty(metadata)
144 .into_alien_error()
145 .context(ErrorData::BuildOperationFailed {
146 binding_name: self.binding_name.clone(),
147 operation: "serialize build metadata".to_string(),
148 })?;
149
150 std::fs::write(&metadata_path, metadata_json)
151 .into_alien_error()
152 .context(ErrorData::BuildOperationFailed {
153 binding_name: self.binding_name.clone(),
154 operation: "write build metadata".to_string(),
155 })?;
156
157 Ok(())
158 }
159
160 fn load_build_metadata(&self, uuid: &str) -> Result<BuildMetadata> {
162 let build_dir = self.base_dir.join("builds").join(uuid);
163 let metadata_path = build_dir.join("metadata.json");
164
165 let metadata_json = std::fs::read_to_string(&metadata_path)
166 .into_alien_error()
167 .context(ErrorData::BuildOperationFailed {
168 binding_name: self.binding_name.clone(),
169 operation: format!("read build metadata for {}", uuid),
170 })?;
171
172 let metadata: BuildMetadata = serde_json::from_str(&metadata_json)
173 .into_alien_error()
174 .context(ErrorData::BuildOperationFailed {
175 binding_name: self.binding_name.clone(),
176 operation: format!("parse build metadata for {}", uuid),
177 })?;
178
179 Ok(metadata)
180 }
181
182 fn is_process_running(&self, pid: u32) -> bool {
184 #[cfg(unix)]
185 {
186 use std::process::Command;
187 Command::new("kill")
189 .args(["-0", &pid.to_string()])
190 .output()
191 .map(|output| output.status.success())
192 .unwrap_or(false)
193 }
194
195 #[cfg(windows)]
196 {
197 use std::process::Command;
198 Command::new("tasklist")
200 .args(["/FI", &format!("PID eq {}", pid)])
201 .output()
202 .map(|output| {
203 output.status.success()
204 && String::from_utf8_lossy(&output.stdout).contains(&pid.to_string())
205 })
206 .unwrap_or(false)
207 }
208 }
209
210 fn update_build_status(&self, metadata: &mut BuildMetadata) -> Result<()> {
212 if metadata.status == BuildStatus::Running {
213 if !self.is_process_running(metadata.pid) {
214 metadata.status = BuildStatus::Succeeded; metadata.end_time = Some(chrono::Utc::now().to_rfc3339());
217 self.save_build_metadata(&metadata.uuid, metadata)?;
218 }
219 }
220 Ok(())
221 }
222}
223
224#[async_trait]
225impl Build for LocalBuild {
226 async fn start_build(&self, config: BuildConfig) -> Result<BuildExecution> {
227 let uuid = Uuid::new_v4().to_string();
228 let start_time = chrono::Utc::now().to_rfc3339();
229 let timestamp = SystemTime::now()
230 .duration_since(UNIX_EPOCH)
231 .unwrap()
232 .as_secs();
233
234 let build_dir = self.create_build_dir(&uuid)?;
236
237 let script_path = build_dir.join("build_script.sh");
239 std::fs::write(&script_path, &config.script)
240 .into_alien_error()
241 .context(ErrorData::BuildOperationFailed {
242 binding_name: self.binding_name.clone(),
243 operation: "write build script".to_string(),
244 })?;
245
246 #[cfg(unix)]
248 {
249 use std::os::unix::fs::PermissionsExt;
250 let mut perms = std::fs::metadata(&script_path)
251 .into_alien_error()
252 .context(ErrorData::BuildOperationFailed {
253 binding_name: self.binding_name.clone(),
254 operation: "get script permissions".to_string(),
255 })?
256 .permissions();
257 perms.set_mode(0o755);
258 std::fs::set_permissions(&script_path, perms)
259 .into_alien_error()
260 .context(ErrorData::BuildOperationFailed {
261 binding_name: self.binding_name.clone(),
262 operation: "set script permissions".to_string(),
263 })?;
264 }
265
266 let mut cmd = Command::new("bash");
268 cmd.arg(&script_path)
269 .current_dir(&build_dir)
270 .stdin(Stdio::null())
271 .stdout(Stdio::piped())
272 .stderr(Stdio::piped());
273
274 let mut merged_environment = self.build_env_vars.clone();
277 merged_environment.extend(config.environment);
278
279 for (key, value) in &merged_environment {
281 cmd.env(key, value);
282 }
283
284 let mut child =
286 cmd.spawn()
287 .into_alien_error()
288 .context(ErrorData::BuildOperationFailed {
289 binding_name: self.binding_name.clone(),
290 operation: "start build process".to_string(),
291 })?;
292
293 let pid = child.id().ok_or_else(|| {
295 AlienError::new(ErrorData::BuildOperationFailed {
296 binding_name: self.binding_name.clone(),
297 operation: "get process ID".to_string(),
298 })
299 })?;
300
301 let build_id = Self::encode_build_id(&uuid, pid, timestamp);
303
304 let metadata = BuildMetadata {
306 uuid: uuid.clone(),
307 pid,
308 start_time: start_time.clone(),
309 end_time: None,
310 status: BuildStatus::Running,
311 };
312 self.save_build_metadata(&uuid, &metadata)?;
313
314 tokio::spawn(async move {
316 let _ = child.wait().await;
317 });
318
319 Ok(BuildExecution {
320 id: build_id,
321 status: BuildStatus::Running,
322 start_time: Some(start_time),
323 end_time: None,
324 })
325 }
326
327 async fn get_build_status(&self, build_id: &str) -> Result<BuildExecution> {
328 let (uuid, _pid, _timestamp) = Self::decode_build_id(build_id)?;
330
331 let mut metadata = self.load_build_metadata(&uuid)?;
333
334 self.update_build_status(&mut metadata)?;
336
337 Ok(BuildExecution {
338 id: build_id.to_string(),
339 status: metadata.status,
340 start_time: Some(metadata.start_time),
341 end_time: metadata.end_time,
342 })
343 }
344
345 async fn stop_build(&self, build_id: &str) -> Result<()> {
346 let (uuid, pid, _timestamp) = Self::decode_build_id(build_id)?;
348
349 let mut metadata = self.load_build_metadata(&uuid)?;
351
352 if metadata.status == BuildStatus::Running {
354 #[cfg(unix)]
355 {
356 use std::process::Command;
357 let _ = Command::new("kill")
359 .args(["-TERM", &pid.to_string()])
360 .output();
361 }
362
363 #[cfg(windows)]
364 {
365 use std::process::Command;
366 let _ = Command::new("taskkill")
368 .args(["/PID", &pid.to_string(), "/F"])
369 .output();
370 }
371
372 metadata.status = BuildStatus::Cancelled;
374 metadata.end_time = Some(chrono::Utc::now().to_rfc3339());
375 self.save_build_metadata(&uuid, &metadata)?;
376 }
377
378 Ok(())
379 }
380}
381
382impl Binding for LocalBuild {}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use std::collections::HashMap;
388 use tempfile::TempDir;
389
390 #[tokio::test]
391 async fn test_local_build_success() {
392 let temp_dir = TempDir::new().unwrap();
393 let local_build =
394 LocalBuild::new_from_path("test-build".to_string(), temp_dir.path().to_path_buf());
395
396 let mut config = BuildConfig {
397 image: "ubuntu:20.04".to_string(), script: "echo 'Hello World!'".to_string(),
399 environment: HashMap::new(),
400 timeout_seconds: 30,
401 compute_type: alien_core::ComputeType::Small,
402 monitoring: None,
403 };
404 config
405 .environment
406 .insert("TEST_VAR".to_string(), "test_value".to_string());
407
408 let execution = local_build.start_build(config).await.unwrap();
409 assert!(!execution.id.is_empty());
410 assert_eq!(execution.status, BuildStatus::Running);
411
412 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
414
415 let status = local_build.get_build_status(&execution.id).await.unwrap();
416 assert_eq!(status.status, BuildStatus::Succeeded);
417 assert!(status.end_time.is_some());
418 }
419
420 #[tokio::test]
421 async fn test_local_build_failure() {
422 let temp_dir = TempDir::new().unwrap();
423 let local_build =
424 LocalBuild::new_from_path("test-build".to_string(), temp_dir.path().to_path_buf());
425
426 let config = BuildConfig {
427 image: "ubuntu:20.04".to_string(), script: "exit 1".to_string(), environment: HashMap::new(),
430 timeout_seconds: 30,
431 compute_type: alien_core::ComputeType::Small,
432 monitoring: None,
433 };
434
435 let execution = local_build.start_build(config).await.unwrap();
436 assert!(!execution.id.is_empty());
437 assert_eq!(execution.status, BuildStatus::Running);
438
439 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
441
442 let status = local_build.get_build_status(&execution.id).await.unwrap();
443 assert_eq!(status.status, BuildStatus::Succeeded); assert!(status.end_time.is_some());
445 }
446
447 #[tokio::test]
448 async fn test_local_build_stop() {
449 let temp_dir = TempDir::new().unwrap();
450 let local_build =
451 LocalBuild::new_from_path("test-build".to_string(), temp_dir.path().to_path_buf());
452
453 let config = BuildConfig {
454 image: "ubuntu:20.04".to_string(), script: "sleep 10".to_string(), environment: HashMap::new(),
457 timeout_seconds: 30,
458 compute_type: alien_core::ComputeType::Small,
459 monitoring: None,
460 };
461
462 let execution = local_build.start_build(config).await.unwrap();
463 assert!(!execution.id.is_empty());
464 assert_eq!(execution.status, BuildStatus::Running);
465
466 local_build.stop_build(&execution.id).await.unwrap();
468
469 let status = local_build.get_build_status(&execution.id).await.unwrap();
470 assert_eq!(status.status, BuildStatus::Cancelled);
471 assert!(status.end_time.is_some());
472 }
473
474 #[test]
475 fn test_build_id_encoding_decoding() {
476 let uuid = "550e8400-e29b-41d4-a716-446655440000";
477 let pid = 12345u32;
478 let timestamp = 1234567890u64;
479
480 let build_id = LocalBuild::encode_build_id(uuid, pid, timestamp);
481 let (decoded_uuid, decoded_pid, decoded_timestamp) =
482 LocalBuild::decode_build_id(&build_id).unwrap();
483
484 assert_eq!(decoded_uuid, uuid);
485 assert_eq!(decoded_pid, pid);
486 assert_eq!(decoded_timestamp, timestamp);
487 }
488}