use crate::logging;
use crate::runtime::container::{ContainerError, ContainerOutput, ContainerRuntime};
use async_trait::async_trait;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
pub struct EmulationRuntime {
workspace: TempDir,
}
impl EmulationRuntime {
pub fn new() -> Self {
let workspace =
tempfile::tempdir().expect("Failed to create temporary workspace for emulation");
EmulationRuntime { workspace }
}
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") {
if let Ok(entry) = entry {
let source = entry.path();
let dest = target_path.join(source.file_name().unwrap());
if source.is_file() {
fs::copy(&source, &dest).expect("Failed to copy file");
} else {
fs::create_dir_all(&dest).expect("Failed to create subdirectory");
}
}
}
}
} else if host_path.is_file() {
fs::copy(host_path, &target_path).expect("Failed to copy file");
}
}
github_workspace
}
}
#[async_trait]
impl ContainerRuntime for EmulationRuntime {
async fn run_container(
&self,
image: &str,
cmd: &[&str],
env_vars: &[(&str, &str)],
working_dir: &Path,
volumes: &[(&Path, &Path)],
) -> Result<ContainerOutput, ContainerError> {
logging::info(&format!("Emulating container: {}", image));
let container_working_dir = self.prepare_workspace(working_dir, volumes);
let contains_nix_command = cmd.iter().any(|&arg| arg.contains("nix "));
if contains_nix_command {
let nix_installed = Command::new("which")
.arg("nix")
.output()
.map(|output| output.status.success())
.unwrap_or(false);
if !nix_installed {
logging::info(&format!(
"⚠️ Nix commands detected but Nix is not installed!"
));
logging::info(&format!(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html"
));
return Ok(ContainerOutput {
stdout: String::new(),
stderr: "Nix is required for this workflow but not installed on your system.\nPlease install Nix first: https://nixos.org/download.html".to_string(),
exit_code: 1,
});
} else {
logging::info(&format!("✅ Nix is installed, proceeding with command"));
}
}
if cmd.is_empty() {
return Err(ContainerError::ContainerExecutionFailed(
"No command specified".to_string(),
));
}
let has_background = cmd.iter().any(|c| c.contains(" &"));
if (cmd[0] == "bash" || cmd[0] == "sh")
&& cmd.len() >= 2
&& (cmd[1] == "-c" || cmd[1] == "-e" || cmd[1] == "-ec")
{
let shell = cmd[0];
let c_flag_index = cmd.iter().position(|&arg| arg == "-c");
if let Some(idx) = c_flag_index {
if idx + 1 < cmd.len() {
let command_str = cmd[idx + 1];
let fixed_cmd = command_str
.replace(">>$GITHUB_OUTPUT", ">>\"$GITHUB_OUTPUT\"")
.replace(">>$GITHUB_ENV", ">>\"$GITHUB_ENV\"")
.replace(">>$GITHUB_PATH", ">>\"$GITHUB_PATH\"")
.replace(">>$GITHUB_STEP_SUMMARY", ">>\"$GITHUB_STEP_SUMMARY\"");
let final_cmd = if has_background && !fixed_cmd.contains(" wait") {
format!("{{ {}; }} && wait", fixed_cmd)
} else {
fixed_cmd
};
let mut command = Command::new(shell);
command.current_dir(&container_working_dir);
for i in 1..idx + 1 {
command.arg(cmd[i]);
}
command.arg(final_cmd);
for (key, value) in env_vars {
command.env(key, value);
}
let output = command
.output()
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
return Ok(ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
});
}
}
}
if has_background {
let mut shell_command = Command::new("sh");
shell_command.current_dir(&container_working_dir);
shell_command.arg("-c");
let command_str = format!("{{ {}; }} && wait", cmd.join(" "));
shell_command.arg(command_str);
for (key, value) in env_vars {
shell_command.env(key, value);
}
let output = shell_command
.output()
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
return Ok(ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
});
}
let mut command = Command::new(cmd[0]);
command.current_dir(&container_working_dir);
for arg in &cmd[1..] {
command.arg(arg);
}
for (key, value) in env_vars {
command.env(key, value);
}
let output = command
.output()
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
Ok(ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
})
}
async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
logging::info(&format!("🔄 Emulation: Pretending to pull image {}", image));
Ok(())
}
async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
logging::info(&format!(
"🔄 Emulation: Pretending to build image {} from {}",
tag,
dockerfile.display()
));
Ok(())
}
}
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 = path.file_name().unwrap();
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> {
if action.starts_with("cachix/install-nix-action") {
logging::info(&format!("🔄 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 {
logging::info(&format!("🔄 Emulation: Nix is required but not installed."));
logging::info(&format!(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html"
));
logging::info(&format!(
"🔄 Continuing emulation, but nix commands will fail."
));
} else {
logging::info(&format!("🔄 Emulation: Using system-installed Nix"));
}
Ok(())
} else {
Ok(())
}
}