use std::path::PathBuf;
use std::process::{Command, Stdio};
use anyhow::{bail, Context, Result};
#[derive(Debug, Clone, Copy, Default)]
pub enum BuildType {
Debug,
#[default]
Release,
RelWithDebInfo,
MinSizeRel,
}
impl std::fmt::Display for BuildType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildType::Debug => write!(f, "Debug"),
BuildType::Release => write!(f, "Release"),
BuildType::RelWithDebInfo => write!(f, "RelWithDebInfo"),
BuildType::MinSizeRel => write!(f, "MinSizeRel"),
}
}
}
#[derive(Debug, Default)]
pub struct CMakeConfig {
source_dir: PathBuf,
build_dir: PathBuf,
install_prefix: Option<PathBuf>,
build_type: BuildType,
variables: Vec<(String, String)>,
cache_variables: Vec<(String, String, String)>,
generator: Option<String>,
toolchain_file: Option<PathBuf>,
jobs: Option<usize>,
verbose: bool,
compile_definitions: Vec<String>,
compiler_cache: Option<super::cache::CacheConfig>,
}
impl CMakeConfig {
pub fn new(source_dir: PathBuf, build_dir: PathBuf) -> Self {
Self {
source_dir,
build_dir,
build_type: BuildType::Release,
..Default::default()
}
}
pub fn build_type(mut self, build_type: BuildType) -> Self {
self.build_type = build_type;
self
}
pub fn install_prefix(mut self, prefix: PathBuf) -> Self {
self.install_prefix = Some(prefix);
self
}
pub fn variable(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.variables.push((name.into(), value.into()));
self
}
pub fn variables(mut self, vars: Vec<(String, String)>) -> Self {
self.variables.extend(vars);
self
}
pub fn cache_variable(
mut self,
name: impl Into<String>,
value: impl Into<String>,
var_type: impl Into<String>,
) -> Self {
self.cache_variables
.push((name.into(), value.into(), var_type.into()));
self
}
pub fn generator(mut self, generator: impl Into<String>) -> Self {
self.generator = Some(generator.into());
self
}
pub fn toolchain_file(mut self, path: PathBuf) -> Self {
self.toolchain_file = Some(path);
self
}
pub fn jobs(mut self, jobs: usize) -> Self {
self.jobs = Some(jobs);
self
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn compile_definition(mut self, definition: impl Into<String>) -> Self {
self.compile_definitions.push(definition.into());
self
}
pub fn compile_definitions(mut self, definitions: Vec<String>) -> Self {
self.compile_definitions.extend(definitions);
self
}
pub fn feature_definitions(mut self, features_str: &str) -> Self {
if !features_str.is_empty() {
for def in features_str.split(';') {
if !def.is_empty() {
self.compile_definitions.push(def.to_string());
}
}
}
self
}
pub fn compiler_cache(mut self, cache: super::cache::CacheConfig) -> Self {
self.compiler_cache = Some(cache);
self
}
fn find_cmake() -> Result<PathBuf> {
which::which("cmake").context("CMake not found. Please install CMake and add it to PATH.")
}
pub fn configure(&self) -> Result<()> {
let cmake = Self::find_cmake()?;
std::fs::create_dir_all(&self.build_dir)
.context("Failed to create CMake build directory")?;
let mut cmd = Command::new(&cmake);
cmd.current_dir(&self.build_dir);
cmd.arg("-S").arg(&self.source_dir);
cmd.arg("-B").arg(&self.build_dir);
cmd.arg(format!("-DCMAKE_BUILD_TYPE={}", self.build_type));
if let Some(prefix) = &self.install_prefix {
cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix.display()));
}
if let Some(generator) = &self.generator {
cmd.arg("-G").arg(generator);
}
if let Some(toolchain) = &self.toolchain_file {
cmd.arg(format!(
"-DCMAKE_TOOLCHAIN_FILE={}",
toolchain.display()
));
}
for (name, value) in &self.variables {
cmd.arg(format!("-D{}={}", name, value));
}
for (name, value, var_type) in &self.cache_variables {
cmd.arg(format!("-D{}:{}={}", name, var_type, value));
}
if let Some(cache) = &self.compiler_cache {
if cache.is_enabled() {
println!(" 🚀 Using {} for compilation caching", cache.cache_type().name());
for (name, value) in cache.cmake_variables() {
cmd.arg(format!("-D{}={}", name, value));
}
}
}
if !self.compile_definitions.is_empty() {
let definitions = self.compile_definitions.join(";");
cmd.arg(format!("-DCCGO_FEATURE_DEFINITIONS={}", definitions));
}
if self.verbose {
eprintln!("Running: {:?}", cmd);
}
let status = cmd
.stdin(Stdio::null())
.status()
.context("Failed to run CMake configure")?;
if !status.success() {
bail!(
"CMake configure failed with exit code: {:?}",
status.code()
);
}
Ok(())
}
pub fn build(&self) -> Result<()> {
let cmake = Self::find_cmake()?;
let mut cmd = Command::new(&cmake);
cmd.arg("--build").arg(&self.build_dir);
if let Some(jobs) = self.jobs {
cmd.arg("-j").arg(jobs.to_string());
} else {
cmd.arg("-j");
}
if self.verbose {
cmd.arg("--verbose");
}
if self.verbose {
eprintln!("Running: {:?}", cmd);
}
let status = cmd
.stdin(Stdio::null())
.status()
.context("Failed to run CMake build")?;
if !status.success() {
bail!("CMake build failed with exit code: {:?}", status.code());
}
Ok(())
}
pub fn install(&self) -> Result<()> {
let cmake = Self::find_cmake()?;
let mut cmd = Command::new(&cmake);
cmd.arg("--install").arg(&self.build_dir);
if self.verbose {
eprintln!("Running: {:?}", cmd);
}
let status = cmd
.stdin(Stdio::null())
.status()
.context("Failed to run CMake install")?;
if !status.success() {
bail!("CMake install failed with exit code: {:?}", status.code());
}
Ok(())
}
pub fn configure_build_install(&self) -> Result<()> {
self.configure()?;
self.build()?;
self.install()?;
Ok(())
}
}
pub fn is_cmake_available() -> bool {
which::which("cmake").is_ok()
}
pub fn cmake_version() -> Option<String> {
let output = Command::new("cmake").arg("--version").output().ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.next()
.and_then(|line| line.strip_prefix("cmake version "))
.map(|v| v.to_string())
}