use std::path::PathBuf;
use std::process::Command;
use anyhow::{bail, Result};
use super::{find_executable, Toolchain};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MingwArch {
X86_64,
I686,
}
impl MingwArch {
pub fn triple_prefix(&self) -> &str {
match self {
MingwArch::X86_64 => "x86_64-w64-mingw32",
MingwArch::I686 => "i686-w64-mingw32",
}
}
pub fn arch_string(&self) -> &str {
match self {
MingwArch::X86_64 => "x86_64",
MingwArch::I686 => "i686",
}
}
}
pub struct MingwToolchain {
gcc_path: PathBuf,
gxx_path: PathBuf,
windres_path: Option<PathBuf>,
arch: MingwArch,
version: String,
}
impl MingwToolchain {
pub fn detect() -> Result<Self> {
Self::detect_for_arch(MingwArch::X86_64)
}
pub fn detect_for_arch(arch: MingwArch) -> Result<Self> {
let prefix = arch.triple_prefix();
let gcc_posix = format!("{}-gcc-posix", prefix);
let gcc_name = format!("{}-gcc", prefix);
let gcc_path = find_executable(&gcc_posix)
.or_else(|| find_executable(&gcc_name))
.ok_or_else(|| {
anyhow::anyhow!(
"MinGW-w64 not found. Please install MinGW-w64 and ensure {} is in PATH.",
gcc_name
)
})?;
let gxx_posix = format!("{}-g++-posix", prefix);
let gxx_name = format!("{}-g++", prefix);
let gxx_path = find_executable(&gxx_posix)
.or_else(|| find_executable(&gxx_name))
.ok_or_else(|| anyhow::anyhow!("MinGW-w64 C++ compiler not found: {}", gxx_name))?;
let windres_name = format!("{}-windres", prefix);
let windres_path = find_executable(&windres_name);
let version = Self::detect_version(&gcc_path)?;
Ok(Self {
gcc_path,
gxx_path,
windres_path,
arch,
version,
})
}
fn detect_version(gcc_path: &PathBuf) -> Result<String> {
let output = Command::new(gcc_path)
.arg("--version")
.output()
.map_err(|e| anyhow::anyhow!("Failed to run {}: {}", gcc_path.display(), e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let first_line = stdout.lines().next().unwrap_or("unknown");
if let Some(version_start) = first_line.find(|c: char| c.is_ascii_digit()) {
let version_part: String = first_line[version_start..]
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
if !version_part.is_empty() {
return Ok(version_part);
}
}
Ok(first_line.to_string())
}
pub fn gcc(&self) -> &PathBuf {
&self.gcc_path
}
pub fn gxx(&self) -> &PathBuf {
&self.gxx_path
}
pub fn windres(&self) -> Option<&PathBuf> {
self.windres_path.as_ref()
}
pub fn arch(&self) -> MingwArch {
self.arch
}
pub fn version(&self) -> &str {
&self.version
}
pub fn cmake_variables_for_arch(&self) -> Vec<(String, String)> {
let prefix = self.arch.triple_prefix();
let mut vars = vec![
("CMAKE_SYSTEM_NAME".to_string(), "Windows".to_string()),
(
"CMAKE_C_COMPILER".to_string(),
self.gcc_path.display().to_string(),
),
(
"CMAKE_CXX_COMPILER".to_string(),
self.gxx_path.display().to_string(),
),
(
"CMAKE_FIND_ROOT_PATH".to_string(),
format!("/usr/{}", prefix),
),
(
"CMAKE_FIND_ROOT_PATH_MODE_PROGRAM".to_string(),
"NEVER".to_string(),
),
(
"CMAKE_FIND_ROOT_PATH_MODE_LIBRARY".to_string(),
"ONLY".to_string(),
),
(
"CMAKE_FIND_ROOT_PATH_MODE_INCLUDE".to_string(),
"ONLY".to_string(),
),
];
if let Some(windres) = &self.windres_path {
vars.push((
"CMAKE_RC_COMPILER".to_string(),
windres.display().to_string(),
));
}
vars
}
pub fn strip_path(&self) -> PathBuf {
let strip_name = format!("{}-strip", self.arch.triple_prefix());
find_executable(&strip_name).unwrap_or_else(|| PathBuf::from(&strip_name))
}
pub fn ar_path(&self) -> PathBuf {
let ar_name = format!("{}-ar", self.arch.triple_prefix());
find_executable(&ar_name).unwrap_or_else(|| PathBuf::from(&ar_name))
}
pub fn merge_static_libs(&self, src_libs: &[PathBuf], dst_lib: &PathBuf) -> Result<()> {
use anyhow::Context;
if src_libs.is_empty() {
bail!("No source libraries to merge");
}
if let Some(parent) = dst_lib.parent() {
std::fs::create_dir_all(parent)?;
}
let temp_dir = std::env::temp_dir().join(format!(
"ccgo-merge-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
));
std::fs::create_dir_all(&temp_dir)?;
let ar_cmd = self.ar_path();
for (idx, lib) in src_libs.iter().enumerate() {
let extract_dir = temp_dir.join(format!("lib{}", idx));
std::fs::create_dir_all(&extract_dir)?;
let output = Command::new(&ar_cmd)
.arg("x")
.arg(lib)
.current_dir(&extract_dir)
.output()
.context("Failed to run ar for extraction")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
std::fs::remove_dir_all(&temp_dir).ok();
bail!("ar extraction failed for {}: {}", lib.display(), stderr);
}
}
let mut object_files: Vec<PathBuf> = Vec::new();
for entry in walkdir::WalkDir::new(&temp_dir) {
let entry = entry?;
let ext = entry.path().extension().and_then(|e| e.to_str());
if matches!(ext, Some("o") | Some("obj")) {
object_files.push(entry.path().to_path_buf());
}
}
if object_files.is_empty() {
std::fs::remove_dir_all(&temp_dir).ok();
bail!("No object files found in source libraries");
}
if dst_lib.exists() {
std::fs::remove_file(dst_lib)?;
}
let mut cmd = Command::new(&ar_cmd);
cmd.arg("rcs").arg(dst_lib);
for obj in &object_files {
cmd.arg(obj);
}
let output = cmd.output().context("Failed to run ar for merging")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
std::fs::remove_dir_all(&temp_dir).ok();
bail!("ar merge failed: {}", stderr);
}
std::fs::remove_dir_all(&temp_dir).ok();
Ok(())
}
}
impl Toolchain for MingwToolchain {
fn name(&self) -> &str {
"mingw-w64"
}
fn is_available(&self) -> bool {
self.gcc_path.exists() && self.gxx_path.exists()
}
fn path(&self) -> Option<PathBuf> {
self.gcc_path.parent().map(|p| p.to_path_buf())
}
fn cmake_variables(&self) -> Vec<(String, String)> {
self.cmake_variables_for_arch()
}
fn validate(&self) -> Result<()> {
if !self.gcc_path.exists() {
bail!("MinGW GCC not found at: {}", self.gcc_path.display());
}
if !self.gxx_path.exists() {
bail!("MinGW G++ not found at: {}", self.gxx_path.display());
}
let test_result = Command::new(&self.gcc_path).args(["--version"]).output();
if test_result.is_err() {
bail!("MinGW GCC failed to execute");
}
Ok(())
}
}
pub fn is_mingw_available() -> bool {
MingwToolchain::detect().is_ok()
}