use crate::{CompilationErrorDetail, Error, Settings};
use regex::Regex;
use std::path::Path;
use std::process::{Command, Output};
fn is_non_windows_os() -> bool {
cfg!(not(windows))
}
fn parse_compiler_output(output_content: &str) -> Result<Vec<CompilationErrorDetail>, ()> {
let mut errors = Vec::new();
let mut lines = output_content.lines();
let first_line = lines.next().unwrap_or("").trim();
let file_path_in_log = first_line .split(" -- ")
.next()
.unwrap_or("")
.trim()
.to_string();
if first_line.contains("Compiled OK.") {
return Ok(Vec::new()); } else if first_line.contains("Compile Failed!") {
let re = Regex::new(r"^\s*line\s+(\d+):\s*(.+?)\s*$").expect("Invalid regex pattern");
for line_str in lines {
let trimmed_line = line_str.trim();
if trimmed_line.is_empty() {
continue;
}
if let Some(caps) = re.captures(trimmed_line) {
let line_num_str = caps.get(1).map_or("", |m| m.as_str());
let line_num = line_num_str.parse::<u32>().ok();
let message = caps.get(2).map_or("", |m| m.as_str()).to_string();
errors.push(CompilationErrorDetail {
file_path_in_log: file_path_in_log.clone(),
line: line_num,
message,
});
} else if !errors.is_empty() && !trimmed_line.starts_with("line ") {
if let Some(last_error) = errors.last_mut() {
last_error.message.push_str("\n"); last_error.message.push_str(trimmed_line); }
}
}
return Ok(errors); }
Err(()) }
pub fn compile_file_impl(
input_file: &Path,
output_log_param: Option<&Path>, compiler_id_param: Option<&str>,
settings: &Settings,
) -> Result<(), Error> {
log::info!(
"Attempting to compile file: {:?}, explicit compiler ID: {:?}, user-requested log: {:?}",
input_file,
compiler_id_param,
output_log_param
);
if !input_file.exists() {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Input file not found: {}", input_file.display()),
)));
}
if !input_file.is_file() {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Input path is not a file: {}", input_file.display()),
)));
}
let compiler_id: String = match compiler_id_param {
Some(id) => id.to_string(),
None => {
let extension = input_file
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| {
Error::InvalidExtension(
input_file
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
)
})?;
settings
.file_associations
.get(extension)
.cloned()
.ok_or_else(|| Error::NoCompilerForExtension(extension.to_string()))?
}
};
log::debug!("Resolved compiler ID to use: {}", compiler_id);
let compiler_info = settings
.installed_compilers
.get(&compiler_id)
.ok_or_else(|| Error::CompilerNotFound(compiler_id.clone()))?;
log::debug!("Using compiler info: {:?}", compiler_info);
let compiler_base_storage_path = crate::config::get_compiler_storage_path(settings)?;
let compiler_executable_path = compiler_base_storage_path
.join(&compiler_info.install_subdir)
.join(&compiler_info.executable_name);
if !compiler_executable_path.exists() {
return Err(Error::CompilerNotFound(format!(
"Executable for '{}' not found at expected path: {}",
compiler_id,
compiler_executable_path.display()
)));
}
log::debug!("Compiler executable: {:?}", compiler_executable_path);
log::debug!("Input CRBasic file: {:?}", input_file);
let mut cmd: Command;
let mut args_for_logging: Vec<String> = Vec::new();
if compiler_info.requires_wine && is_non_windows_os() {
let wine_exe = settings.wine_path.as_deref().unwrap_or("wine");
cmd = Command::new(wine_exe);
cmd.arg(compiler_executable_path.to_string_lossy().as_ref()); args_for_logging.push(wine_exe.to_string());
args_for_logging.push(compiler_executable_path.to_string_lossy().into_owned());
log::info!("Using Wine. Wine executable: {}", wine_exe);
} else {
cmd = Command::new(&compiler_executable_path);
args_for_logging.push(compiler_executable_path.to_string_lossy().into_owned());
log::info!("Running compiler natively (Windows or requires_wine=false).");
}
let input_file_str = input_file.to_string_lossy().into_owned();
cmd.arg(&input_file_str);
args_for_logging.push(input_file_str.clone());
if let Some(log_path) = output_log_param {
let output_log_path_str = log_path.to_string_lossy().into_owned();
cmd.arg(&output_log_path_str);
args_for_logging.push(output_log_path_str);
log::debug!("Compiler will create log at user-specified path: {:?}", log_path);
} else {
log::debug!("Compiler will output to stdout/stderr (no explicit log file argument passed).");
}
log::info!("Executing command: {}", args_for_logging.join(" "));
let execution_result: Result<Output, std::io::Error> = cmd.output();
match execution_result {
Ok(output) => {
log::debug!("Compiler process finished. Status: {}", output.status);
let stdout_content = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr_content = String::from_utf8_lossy(&output.stderr).into_owned();
if !stdout_content.trim().is_empty() {
log::debug!(" Stdout from compiler process:\n{}", stdout_content.trim());
}
if !stderr_content.trim().is_empty() {
log::warn!(" Stderr from compiler process:\n{}", stderr_content.trim());
}
match parse_compiler_output(&stdout_content) {
Ok(parsed_errors) => { if parsed_errors.is_empty() { log::info!("Compilation successful for {:?}.", input_file);
println!("✅ Successfully compiled: {}", input_file.display());
if let Some(log_p) = output_log_param {
println!(" Compiler log created at: {}", log_p.display());
}
Ok(())
} else { log::error!("Compilation failed for {:?} based on stdout parsing.", input_file);
Err(Error::CompilationFailed {
file_path: input_file.to_path_buf(),
errors: parsed_errors,
raw_log: stdout_content, })
}
}
Err(_) => { log::warn!(
"Unrecognized compiler stdout format for {:?}. Relying on process exit status.",
input_file
);
if output.status.success() {
log::info!(
"Compiler process for {:?} exited successfully despite unrecognized stdout. Assuming success.",
input_file
);
println!(
"✅ Compilation process for {} finished successfully (exit code 0), but output format was unrecognized.",
input_file.display()
);
if let Some(log_p) = output_log_param {
println!(" Compiler log (if created by compiler): {}", log_p.display());
}
if !stdout_content.trim().is_empty() {
println!(" Compiler output (stdout):\n{}", stdout_content.trim());
}
Ok(())
} else {
log::error!(
"Compiler process for {:?} failed (Exit Code: {:?}) and stdout format was unrecognized.",
input_file, output.status.code()
);
Err(Error::GenericCompilationFailedWithLog {
file_path: input_file.to_path_buf(),
raw_log: format!(
"Exit Code: {:?}\nStdout:\n{}\nStderr:\n{}",
output.status.code(),
stdout_content.trim(),
stderr_content.trim()
),
})
}
}
}
}
Err(e) => {
log::error!("Failed to execute compiler process: {}", e);
if compiler_info.requires_wine && is_non_windows_os() && e.kind() == std::io::ErrorKind::NotFound {
let wine_exe_check = settings.wine_path.as_deref().unwrap_or("wine");
if e.to_string().contains(wine_exe_check) || e.to_string().contains("No such file or directory") {
return Err(Error::WineNotFound);
}
}
Err(Error::Subprocess(e))
}
}
}