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,
Aarch64,
}
impl MingwArch {
pub fn triple_prefix(&self) -> &str {
match self {
MingwArch::X86_64 => "x86_64-w64-mingw32",
MingwArch::I686 => "i686-w64-mingw32",
MingwArch::Aarch64 => "aarch64-w64-mingw32",
}
}
pub fn arch_string(&self) -> &str {
match self {
MingwArch::X86_64 => "x86_64",
MingwArch::I686 => "i686",
MingwArch::Aarch64 => "aarch64",
}
}
pub fn parse(s: &str) -> Result<Self> {
match s {
"x86_64" | "x64" => Ok(MingwArch::X86_64),
"i686" | "x86" | "i386" => Ok(MingwArch::I686),
"aarch64" | "arm64" => Ok(MingwArch::Aarch64),
other => bail!("Unknown MinGW architecture '{}'. Supported: x86_64, i686, aarch64", other),
}
}
}
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 clang_name = format!("{}-clang", prefix);
let gcc_path = find_executable(&gcc_posix)
.or_else(|| find_executable(&gcc_name))
.or_else(|| find_executable(&clang_name))
.ok_or_else(|| {
let install_hint = if arch == MingwArch::Aarch64 {
"Install llvm-mingw: https://github.com/mstorsjo/llvm-mingw/releases\n \
Then add its bin/ directory to PATH."
} else {
"Install MinGW-w64: apt-get install mingw-w64 (or equivalent)"
};
anyhow::anyhow!(
"MinGW cross-compiler not found for target {}.\n {}\n \
Looked for: {}, {}, {}",
prefix,
install_hint,
gcc_posix,
gcc_name,
clang_name,
)
})?;
let gxx_posix = format!("{}-g++-posix", prefix);
let gxx_name = format!("{}-g++", prefix);
let clangxx_name = format!("{}-clang++", prefix);
let gxx_path = find_executable(&gxx_posix)
.or_else(|| find_executable(&gxx_name))
.or_else(|| find_executable(&clangxx_name))
.ok_or_else(|| {
anyhow::anyhow!(
"MinGW C++ compiler not found for target {}.\n \
Looked for: {}, {}, {}",
prefix,
gxx_posix,
gxx_name,
clangxx_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()
}