use crate::container::{ContainerError, ContainerOutput, ContainerRuntime};
use async_trait::async_trait;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
use tempfile::TempDir;
use which;
use wrkflw_logging;
static EMULATION_WORKSPACES: Lazy<Mutex<Vec<PathBuf>>> = Lazy::new(|| Mutex::new(Vec::new()));
static EMULATION_PROCESSES: Lazy<Mutex<Vec<u32>>> = Lazy::new(|| Mutex::new(Vec::new()));
pub struct EmulationRuntime {
#[allow(dead_code)]
workspace: TempDir,
}
impl Default for EmulationRuntime {
fn default() -> Self {
Self::new()
}
}
impl EmulationRuntime {
pub fn new() -> Self {
let workspace =
tempfile::tempdir().expect("Failed to create temporary workspace for emulation");
if let Ok(mut workspaces) = EMULATION_WORKSPACES.lock() {
workspaces.push(workspace.path().to_path_buf());
}
EmulationRuntime { workspace }
}
#[allow(dead_code)]
fn prepare_workspace(&self, _working_dir: &Path, volumes: &[(&Path, &Path)]) -> PathBuf {
let container_root = self.workspace.path().to_path_buf();
let github_workspace = container_root.join("github").join("workspace");
fs::create_dir_all(&github_workspace)
.expect("Failed to create github/workspace directory structure");
for (host_path, container_path) in volumes {
let target_path = if container_path.starts_with("/github/workspace") {
let rel_path = container_path
.strip_prefix("/github/workspace")
.unwrap_or(Path::new(""));
github_workspace.join(rel_path)
} else if container_path.starts_with("/") {
container_root.join(container_path.strip_prefix("/").unwrap_or(container_path))
} else {
container_root.join(container_path)
};
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent).expect("Failed to create directory structure");
}
if host_path.is_dir() {
if *container_path == Path::new("/github/workspace") {
copy_directory_contents(host_path, &github_workspace)
.expect("Failed to copy project files to workspace");
} else {
fs::create_dir_all(&target_path).expect("Failed to create target directory");
for entry in fs::read_dir(host_path)
.expect("Failed to read source directory")
.flatten()
{
let source = entry.path();
let file_name = match source.file_name() {
Some(name) => name,
None => {
eprintln!(
"Warning: Could not get file name from path: {:?}",
source
);
continue; }
};
let dest = target_path.join(file_name);
if source.is_file() {
if let Err(e) = fs::copy(&source, &dest) {
eprintln!(
"Warning: Failed to copy file from {:?} to {:?}: {}",
&source, &dest, e
);
}
} else {
fs::create_dir_all(&dest).expect("Failed to create subdirectory");
}
}
}
} else if host_path.is_file() {
let file_name = match host_path.file_name() {
Some(name) => name,
None => {
eprintln!(
"Warning: Could not get file name from path: {:?}",
host_path
);
continue; }
};
let dest = target_path.join(file_name);
if let Err(e) = fs::copy(host_path, &dest) {
eprintln!(
"Warning: Failed to copy file from {:?} to {:?}: {}",
host_path, &dest, e
);
}
}
}
github_workspace
}
}
#[async_trait]
impl ContainerRuntime for EmulationRuntime {
async fn run_container(
&self,
_image: &str,
command: &[&str],
env_vars: &[(&str, &str)],
working_dir: &Path,
_volumes: &[(&Path, &Path)],
) -> Result<ContainerOutput, ContainerError> {
let mut command_str = String::new();
for part in command {
if !command_str.is_empty() {
command_str.push(' ');
}
command_str.push_str(part);
}
wrkflw_logging::info(&format!("Executing command in container: {}", command_str));
wrkflw_logging::info(&format!("Working directory: {}", working_dir.display()));
wrkflw_logging::info(&format!("Command length: {}", command.len()));
if command.is_empty() {
return Err(ContainerError::ContainerExecution(
"Empty command array".to_string(),
));
}
for (i, part) in command.iter().enumerate() {
wrkflw_logging::info(&format!("Command part {}: '{}'", i, part));
}
wrkflw_logging::info("Environment variables:");
for (key, value) in env_vars {
wrkflw_logging::info(&format!(" {}={}", key, value));
}
let actual_working_dir: PathBuf = if !working_dir.exists() {
let mut workspace_path = None;
for (key, value) in env_vars {
if *key == "GITHUB_WORKSPACE" || *key == "CI_PROJECT_DIR" {
workspace_path = Some(PathBuf::from(value));
break;
}
}
if let Some(path) = workspace_path {
if path.exists() {
wrkflw_logging::info(&format!(
"Using environment-defined workspace: {}",
path.display()
));
path
} else {
let current_dir =
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
wrkflw_logging::info(&format!(
"Using current directory: {}",
current_dir.display()
));
current_dir
}
} else {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
wrkflw_logging::info(&format!(
"Using current directory: {}",
current_dir.display()
));
current_dir
}
} else {
working_dir.to_path_buf()
};
wrkflw_logging::info(&format!(
"Using actual working directory: {}",
actual_working_dir.display()
));
let command_path = which::which(command[0]);
match &command_path {
Ok(path) => wrkflw_logging::info(&format!("Found command at: {}", path.display())),
Err(e) => wrkflw_logging::error(&format!(
"Command not found in PATH: {} - Error: {}",
command[0], e
)),
}
if command_str.starts_with("echo ")
|| command_str.starts_with("cp ")
|| command_str.starts_with("mkdir ")
|| command_str.starts_with("mv ")
{
wrkflw_logging::info("Executing as shell command");
let mut cmd = Command::new("sh");
cmd.arg("-c");
cmd.arg(&command_str);
cmd.current_dir(&actual_working_dir);
for (key, value) in env_vars {
cmd.env(key, value);
}
match cmd.output() {
Ok(output_result) => {
let exit_code = output_result.status.code().unwrap_or(-1);
let output = String::from_utf8_lossy(&output_result.stdout).to_string();
let error = String::from_utf8_lossy(&output_result.stderr).to_string();
wrkflw_logging::debug(&format!(
"Shell command completed with exit code: {}",
exit_code
));
if exit_code != 0 {
let mut error_details = format!(
"Command failed with exit code: {}\nCommand: {}\n\nError output:\n{}",
exit_code, command_str, error
);
error_details.push_str("\n\nEnvironment variables:\n");
for (key, value) in env_vars {
if key.starts_with("GITHUB_") || key.starts_with("CI_") {
error_details.push_str(&format!("{}={}\n", key, value));
}
}
return Err(ContainerError::ContainerExecution(error_details));
}
return Ok(ContainerOutput {
stdout: output,
stderr: error,
exit_code,
});
}
Err(e) => {
return Err(ContainerError::ContainerExecution(format!(
"Failed to execute command: {}\nError: {}",
command_str, e
)));
}
}
}
if command_str.starts_with("cargo ") || command_str.starts_with("rustup ") {
let parts: Vec<&str> = command_str.split_whitespace().collect();
if parts.is_empty() {
return Err(ContainerError::ContainerExecution(
"Empty command".to_string(),
));
}
let mut cmd = Command::new(parts[0]);
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
wrkflw_logging::info(&format!(
"Using project directory for Rust command: {}",
current_dir.display()
));
cmd.current_dir(¤t_dir);
for (key, value) in env_vars {
if *key == "CARGO_HOME" && value.contains("${CI_PROJECT_DIR}") {
let cargo_home =
value.replace("${CI_PROJECT_DIR}", ¤t_dir.to_string_lossy());
wrkflw_logging::info(&format!("Setting CARGO_HOME to: {}", cargo_home));
cmd.env(key, cargo_home);
} else {
cmd.env(key, value);
}
}
if parts.len() > 1 {
cmd.args(&parts[1..]);
}
wrkflw_logging::debug(&format!(
"Executing Rust command: {} in {}",
command_str,
current_dir.display()
));
match cmd.output() {
Ok(output_result) => {
let exit_code = output_result.status.code().unwrap_or(-1);
let output = String::from_utf8_lossy(&output_result.stdout).to_string();
let error = String::from_utf8_lossy(&output_result.stderr).to_string();
wrkflw_logging::debug(&format!("Command exit code: {}", exit_code));
if exit_code != 0 {
let mut error_details = format!(
"Command failed with exit code: {}\nCommand: {}\n\nError output:\n{}",
exit_code, command_str, error
);
error_details.push_str("\n\nEnvironment variables:\n");
for (key, value) in env_vars {
if key.starts_with("GITHUB_")
|| key.starts_with("RUST")
|| key.starts_with("CARGO")
|| key.starts_with("CI_")
{
error_details.push_str(&format!("{}={}\n", key, value));
}
}
return Err(ContainerError::ContainerExecution(error_details));
}
return Ok(ContainerOutput {
stdout: output,
stderr: error,
exit_code,
});
}
Err(e) => {
return Err(ContainerError::ContainerExecution(format!(
"Failed to execute Rust command: {}",
e
)));
}
}
}
let mut cmd = Command::new("sh");
cmd.arg("-c");
cmd.arg(&command_str);
cmd.current_dir(&actual_working_dir);
for (key, value) in env_vars {
cmd.env(key, value);
}
match cmd.output() {
Ok(output_result) => {
let exit_code = output_result.status.code().unwrap_or(-1);
let output = String::from_utf8_lossy(&output_result.stdout).to_string();
let error = String::from_utf8_lossy(&output_result.stderr).to_string();
wrkflw_logging::debug(&format!("Command completed with exit code: {}", exit_code));
if exit_code != 0 {
let mut error_details = format!(
"Command failed with exit code: {}\nCommand: {}\n\nError output:\n{}",
exit_code, command_str, error
);
error_details.push_str("\n\nEnvironment variables:\n");
for (key, value) in env_vars {
if key.starts_with("GITHUB_") || key.starts_with("CI_") {
error_details.push_str(&format!("{}={}\n", key, value));
}
}
return Err(ContainerError::ContainerExecution(error_details));
}
Ok(ContainerOutput {
stdout: format!(
"Emulated container execution with command: {}\n\nOutput:\n{}",
command_str, output
),
stderr: error,
exit_code,
})
}
Err(e) => {
return Err(ContainerError::ContainerExecution(format!(
"Failed to execute command: {}\nError: {}",
command_str, e
)));
}
}
}
async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
wrkflw_logging::info(&format!("🔄 Emulation: Pretending to pull image {}", image));
Ok(())
}
async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
wrkflw_logging::info(&format!(
"🔄 Emulation: Pretending to build image {} from {}",
tag,
dockerfile.display()
));
Ok(())
}
async fn prepare_language_environment(
&self,
language: &str,
version: Option<&str>,
_additional_packages: Option<Vec<String>>,
) -> Result<String, ContainerError> {
let base_image = match language {
"python" => version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v)),
"node" => version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v)),
"java" => version.map_or("eclipse-temurin:17-jdk".to_string(), |v| {
format!("eclipse-temurin:{}", v)
}),
"go" => version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v)),
"dotnet" => version.map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| {
format!("mcr.microsoft.com/dotnet/sdk:{}", v)
}),
"rust" => version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v)),
_ => {
return Err(ContainerError::ContainerStart(format!(
"Unsupported language: {}",
language
)))
}
};
Ok(base_image)
}
}
#[allow(dead_code)]
fn copy_directory_contents(source: &Path, dest: &Path) -> std::io::Result<()> {
fs::create_dir_all(dest)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let path = entry.path();
let file_name = match path.file_name() {
Some(name) => name,
None => {
eprintln!("Warning: Could not get file name from path: {:?}", path);
continue; }
};
let dest_path = dest.join(file_name);
let file_name_str = file_name.to_string_lossy();
if file_name_str.starts_with(".")
&& file_name_str != ".gitignore"
&& file_name_str != ".github"
{
continue;
}
if file_name_str == "target" {
continue;
}
if path.is_dir() {
copy_directory_contents(&path, &dest_path)?;
} else {
fs::copy(&path, &dest_path)?;
}
}
Ok(())
}
pub async fn handle_special_action(action: &str) -> Result<(), ContainerError> {
let action_parts: Vec<&str> = action.split('@').collect();
let action_name = action_parts[0];
let action_version = if action_parts.len() > 1 {
action_parts[1]
} else {
"latest"
};
wrkflw_logging::info(&format!(
"🔄 Processing action: {} @ {}",
action_name, action_version
));
if action.starts_with("cachix/install-nix-action") {
wrkflw_logging::info("🔄 Emulating cachix/install-nix-action");
let nix_installed = Command::new("which")
.arg("nix")
.output()
.map(|output| output.status.success())
.unwrap_or(false);
if !nix_installed {
wrkflw_logging::info("🔄 Emulation: Nix is required but not installed.");
wrkflw_logging::info(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html",
);
wrkflw_logging::info("🔄 Continuing emulation, but nix commands will fail.");
} else {
wrkflw_logging::info("🔄 Emulation: Using system-installed Nix");
}
} else if action.starts_with("actions-rs/cargo@") {
wrkflw_logging::info(&format!("🔄 Detected Rust cargo action: {}", action));
check_command_available("cargo", "Rust/Cargo", "https://rustup.rs/");
} else if action.starts_with("actions-rs/toolchain@") {
wrkflw_logging::info(&format!("🔄 Detected Rust toolchain action: {}", action));
check_command_available("rustc", "Rust", "https://rustup.rs/");
} else if action.starts_with("actions-rs/fmt@") {
wrkflw_logging::info(&format!("🔄 Detected Rust formatter action: {}", action));
check_command_available("rustfmt", "rustfmt", "rustup component add rustfmt");
} else if action.starts_with("actions/setup-node@") {
wrkflw_logging::info(&format!("🔄 Detected Node.js setup action: {}", action));
check_command_available("node", "Node.js", "https://nodejs.org/");
} else if action.starts_with("actions/setup-python@") {
wrkflw_logging::info(&format!("🔄 Detected Python setup action: {}", action));
check_command_available("python", "Python", "https://www.python.org/downloads/");
} else if action.starts_with("actions/setup-java@") {
wrkflw_logging::info(&format!("🔄 Detected Java setup action: {}", action));
check_command_available("java", "Java", "https://adoptium.net/");
} else if action.starts_with("actions/checkout@") {
wrkflw_logging::info("🔄 Detected checkout action - workspace files are already prepared");
} else if action.starts_with("actions/cache@") {
wrkflw_logging::info(
"🔄 Detected cache action - caching is not fully supported in emulation mode",
);
} else {
wrkflw_logging::info(&format!(
"🔄 Action '{}' has no special handling in emulation mode",
action_name
));
}
Ok(())
}
fn check_command_available(command: &str, name: &str, install_url: &str) {
let is_available = Command::new("which")
.arg(command)
.output()
.map(|output| output.status.success())
.unwrap_or(false);
if !is_available {
wrkflw_logging::warning(&format!("{} is required but not found on the system", name));
wrkflw_logging::info(&format!(
"To use this action, please install {}: {}",
name, install_url
));
wrkflw_logging::info(&format!(
"Continuing emulation, but {} commands will fail",
name
));
} else {
if let Ok(output) = Command::new(command).arg("--version").output() {
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout);
wrkflw_logging::info(&format!("🔄 Using system {}: {}", name, version.trim()));
}
}
}
}
#[allow(dead_code)]
fn add_action_env_vars(
env_map: &mut HashMap<String, String>,
action: &str,
with_params: &Option<HashMap<String, String>>,
) {
if let Some(params) = with_params {
if action.starts_with("actions/setup-node") {
if let Some(version) = params.get("node-version") {
env_map.insert("NODE_VERSION".to_string(), version.clone());
}
env_map.insert(
"NPM_CONFIG_PREFIX".to_string(),
"/tmp/.npm-global".to_string(),
);
env_map.insert("PATH".to_string(), "/tmp/.npm-global/bin:$PATH".to_string());
} else if action.starts_with("actions/setup-python") {
if let Some(version) = params.get("python-version") {
env_map.insert("PYTHON_VERSION".to_string(), version.clone());
}
env_map.insert("PIP_CACHE_DIR".to_string(), "/tmp/.pip-cache".to_string());
} else if action.starts_with("actions/setup-java") {
if let Some(version) = params.get("java-version") {
env_map.insert("JAVA_VERSION".to_string(), version.clone());
}
env_map.insert(
"JAVA_HOME".to_string(),
"/usr/lib/jvm/default-java".to_string(),
);
}
}
}
pub async fn cleanup_resources() {
cleanup_processes().await;
cleanup_workspaces().await;
}
async fn cleanup_processes() {
let processes_to_cleanup = {
if let Ok(processes) = EMULATION_PROCESSES.lock() {
processes.clone()
} else {
vec![]
}
};
for pid in processes_to_cleanup {
wrkflw_logging::info(&format!("Cleaning up emulated process: {}", pid));
#[cfg(unix)]
{
let _ = Command::new("kill")
.arg("-TERM")
.arg(pid.to_string())
.output();
}
#[cfg(windows)]
{
let _ = Command::new("taskkill")
.arg("/F")
.arg("/PID")
.arg(&pid.to_string())
.output();
}
if let Ok(mut processes) = EMULATION_PROCESSES.lock() {
processes.retain(|p| *p != pid);
}
}
}
async fn cleanup_workspaces() {
let workspaces_to_cleanup = {
if let Ok(workspaces) = EMULATION_WORKSPACES.lock() {
workspaces.clone()
} else {
vec![]
}
};
for workspace_path in workspaces_to_cleanup {
wrkflw_logging::info(&format!(
"Cleaning up emulation workspace: {}",
workspace_path.display()
));
if workspace_path.exists() {
match fs::remove_dir_all(&workspace_path) {
Ok(_) => wrkflw_logging::info("Successfully removed workspace directory"),
Err(e) => wrkflw_logging::error(&format!("Error removing workspace: {}", e)),
}
}
if let Ok(mut workspaces) = EMULATION_WORKSPACES.lock() {
workspaces.retain(|w| *w != workspace_path);
}
}
}
#[allow(dead_code)]
pub fn track_process(pid: u32) {
if let Ok(mut processes) = EMULATION_PROCESSES.lock() {
processes.push(pid);
}
}
#[allow(dead_code)]
pub fn untrack_process(pid: u32) {
if let Ok(mut processes) = EMULATION_PROCESSES.lock() {
processes.retain(|p| *p != pid);
}
}
#[allow(dead_code)]
pub fn track_workspace(path: &Path) {
if let Ok(mut workspaces) = EMULATION_WORKSPACES.lock() {
workspaces.push(path.to_path_buf());
}
}
#[allow(dead_code)]
pub fn untrack_workspace(path: &Path) {
if let Ok(mut workspaces) = EMULATION_WORKSPACES.lock() {
workspaces.retain(|w| *w != path);
}
}
#[cfg(test)]
pub fn get_tracked_workspaces() -> Vec<PathBuf> {
if let Ok(workspaces) = EMULATION_WORKSPACES.lock() {
workspaces.clone()
} else {
vec![]
}
}
#[cfg(test)]
pub fn get_tracked_processes() -> Vec<u32> {
if let Ok(processes) = EMULATION_PROCESSES.lock() {
processes.clone()
} else {
vec![]
}
}