use std::path::PathBuf;
use std::process::Command;
use std::time::Instant;
use anyhow::{bail, Context, Result};
use crate::build::archive::{
get_unified_include_path, ArchiveBuilder, ARCHIVE_DIR_SHARED, ARCHIVE_DIR_STATIC,
};
use crate::build::cmake::{BuildType, CMakeConfig};
use crate::build::{BuildContext, BuildResult, PlatformBuilder};
use crate::commands::build::LinkType;
pub struct ConanBuilder;
impl ConanBuilder {
pub fn new() -> Self {
Self
}
fn check_conan_installed() -> Result<String> {
let output = Command::new("conan")
.arg("--version")
.output()
.context("Failed to run 'conan --version'. Is Conan installed?")?;
if !output.status.success() {
bail!("Conan is not installed or not in PATH.\nPlease install: pip install conan");
}
let version = String::from_utf8_lossy(&output.stdout);
Ok(version.trim().to_string())
}
fn detect_host_arch() -> String {
#[cfg(target_arch = "x86_64")]
{
"x86_64".to_string()
}
#[cfg(target_arch = "aarch64")]
{
"arm64".to_string()
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
std::env::consts::ARCH.to_string()
}
}
fn build_link_type(&self, ctx: &BuildContext, link_type: &str) -> Result<PathBuf> {
let build_dir = ctx.cmake_build_dir.join(link_type);
let install_dir = build_dir.join("install");
std::fs::create_dir_all(&build_dir)?;
std::fs::create_dir_all(&install_dir)?;
let build_shared = link_type == "shared";
let build_type = if ctx.options.release {
"Release"
} else {
"Debug"
};
let conanfile = if ctx.project_root.join("conanfile.py").exists() {
ctx.project_root.join("conanfile.py")
} else if ctx.project_root.join("conan").join("conanfile.py").exists() {
ctx.project_root.join("conan").join("conanfile.py")
} else {
return self.build_with_cmake(ctx, link_type);
};
let conanfile_dir = conanfile.parent().unwrap();
if ctx.options.verbose {
eprintln!("Using conanfile: {}", conanfile.display());
}
let ccgo_cmake_dir = ctx.ccgo_cmake_dir();
if ctx.options.verbose {
if let Some(ref dir) = ccgo_cmake_dir {
eprintln!("CCGO_CMAKE_DIR: {}", dir.display());
}
}
let mut install_cmd = Command::new("conan");
install_cmd
.current_dir(conanfile_dir)
.arg("install")
.arg(".")
.arg("--output-folder")
.arg(build_dir.display().to_string())
.arg("--build=missing")
.arg(format!("-s:h=build_type={}", build_type));
if let Some(ref cmake_dir) = ccgo_cmake_dir {
install_cmd.env("CCGO_CMAKE_DIR", cmake_dir);
}
if build_shared {
install_cmd.arg("-o:h=*:shared=True");
} else {
install_cmd.arg("-o:h=*:shared=False");
}
install_cmd.env("CCGO_BUILD_SHARED", if build_shared { "ON" } else { "OFF" });
if ctx.options.verbose {
eprintln!(
"Running: conan install {:?}",
install_cmd.get_args().collect::<Vec<_>>()
);
}
let status = install_cmd
.status()
.context("Failed to run conan install")?;
if !status.success() {
bail!("conan install failed");
}
let mut build_cmd = Command::new("conan");
build_cmd
.current_dir(conanfile_dir)
.arg("build")
.arg(".")
.arg("--output-folder")
.arg(build_dir.display().to_string());
if let Some(ref cmake_dir) = ccgo_cmake_dir {
build_cmd.env("CCGO_CMAKE_DIR", cmake_dir);
}
build_cmd.env("CCGO_BUILD_SHARED", if build_shared { "ON" } else { "OFF" });
if ctx.options.verbose {
eprintln!(
"Running: conan build {:?}",
build_cmd.get_args().collect::<Vec<_>>()
);
}
let status = build_cmd.status().context("Failed to run conan build")?;
if !status.success() {
bail!("conan build failed");
}
Ok(build_dir)
}
fn build_with_cmake(&self, ctx: &BuildContext, link_type: &str) -> Result<PathBuf> {
let build_dir = ctx.cmake_build_dir.join(link_type);
let install_dir = build_dir.join("install");
let build_shared = link_type == "shared";
let mut cmake = CMakeConfig::new(ctx.project_root.clone(), build_dir.clone())
.build_type(if ctx.options.release {
BuildType::Release
} else {
BuildType::Debug
})
.install_prefix(install_dir.clone())
.variable("CCGO_BUILD_STATIC", if build_shared { "OFF" } else { "ON" })
.variable("CCGO_BUILD_SHARED", if build_shared { "ON" } else { "OFF" })
.variable(
"CCGO_BUILD_SHARED_LIBS",
if build_shared { "ON" } else { "OFF" },
)
.variable("CCGO_LIB_NAME", ctx.lib_name())
.jobs(ctx.jobs())
.verbose(ctx.options.verbose);
if let Some(cmake_dir) = ctx.ccgo_cmake_dir() {
cmake = cmake.variable("CCGO_CMAKE_DIR", cmake_dir.display().to_string());
}
cmake = cmake.variable(
"CCGO_CONFIG_PRESET_VISIBILITY",
ctx.symbol_visibility().to_string(),
);
if let Some(deps_map) = ctx.deps_map() {
cmake = cmake.variable("CCGO_CONFIG_DEPS_MAP", deps_map);
}
if let Ok(feature_defines) = ctx.cmake_feature_defines() {
if !feature_defines.is_empty() {
cmake = cmake.feature_definitions(&feature_defines);
if ctx.options.verbose {
eprintln!(
" Enabled features: {}",
feature_defines.replace(';', ", ")
);
}
}
}
cmake.configure_build_install()?;
Ok(build_dir)
}
fn find_lib_dir(&self, build_dir: &PathBuf, _is_release: bool) -> Option<PathBuf> {
let possible_dirs = vec![
build_dir.join("out"), build_dir.join("build/Release"), build_dir.join("build/Debug"), build_dir.join("install/lib"), build_dir.join("lib"), build_dir.join("build/lib"), ];
let lib_extensions = ["a", "so", "dylib", "lib", "dll"];
for dir in &possible_dirs {
if !dir.exists() {
continue;
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if lib_extensions.contains(&ext) {
return Some(dir.clone());
}
}
}
}
}
}
None
}
fn find_include_dir(&self, build_dir: &PathBuf) -> Option<PathBuf> {
let possible_dirs = vec![
build_dir.join("install/include"),
build_dir.join("include"),
build_dir.join("build/include"),
];
for dir in possible_dirs {
if dir.exists() {
return Some(dir);
}
}
None
}
}
impl PlatformBuilder for ConanBuilder {
fn platform_name(&self) -> &str {
"conan"
}
fn default_architectures(&self) -> Vec<String> {
vec![Self::detect_host_arch()]
}
fn validate_prerequisites(&self, ctx: &BuildContext) -> Result<()> {
let version =
Self::check_conan_installed().context("Conan build requires Conan to be installed")?;
if ctx.options.verbose {
eprintln!("Found {}", version);
}
if !crate::build::cmake::is_cmake_available() {
bail!("CMake is required for Conan builds. Please install CMake.");
}
if ctx.options.verbose {
eprintln!("Conan prerequisites validated");
}
Ok(())
}
fn build(&self, ctx: &BuildContext) -> Result<BuildResult> {
let start = Instant::now();
self.validate_prerequisites(ctx)?;
if ctx.options.verbose {
eprintln!("Building {} for Conan...", ctx.lib_name());
}
std::fs::create_dir_all(&ctx.output_dir)?;
let archive = ArchiveBuilder::new(
ctx.lib_name(),
ctx.version(),
ctx.publish_suffix(),
ctx.options.release,
"conan",
ctx.output_dir.clone(),
)?;
let host_arch = Self::detect_host_arch();
let mut built_link_types = Vec::new();
if matches!(ctx.options.link_type, LinkType::Static | LinkType::Both) {
if ctx.options.verbose {
eprintln!("Building static library...");
}
let build_dir = self.build_link_type(ctx, "static")?;
if let Some(lib_dir) = self.find_lib_dir(&build_dir, ctx.options.release) {
let archive_path = format!("lib/{}/{}", self.platform_name(), ARCHIVE_DIR_STATIC);
archive.add_files_flat(&lib_dir, &archive_path, &["a", "lib"])?;
}
built_link_types.push("static");
}
if matches!(ctx.options.link_type, LinkType::Shared | LinkType::Both) {
if ctx.options.verbose {
eprintln!("Building shared library...");
}
let build_dir = self.build_link_type(ctx, "shared")?;
if let Some(lib_dir) = self.find_lib_dir(&build_dir, ctx.options.release) {
let archive_path = format!("lib/{}/{}", self.platform_name(), ARCHIVE_DIR_SHARED);
#[cfg(target_os = "windows")]
let extensions = &["dll", "lib"];
#[cfg(target_os = "macos")]
let extensions = &["dylib", "so", "a"]; #[cfg(target_os = "linux")]
let extensions = &["so", "a"];
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
let extensions = &["so", "dylib", "dll", "a"];
archive.add_files_flat(&lib_dir, &archive_path, extensions)?;
}
built_link_types.push("shared");
}
let include_source = ctx.include_source_dir();
if include_source.exists() {
let include_path = get_unified_include_path(ctx.lib_name(), &include_source);
archive.add_directory(&include_source, &include_path)?;
if ctx.options.verbose {
eprintln!(
"Added include files from {} to {}",
include_source.display(),
include_path
);
}
}
let link_type_str = ctx.options.link_type.to_string();
let sdk_archive = archive.create_sdk_archive(&[host_arch.clone()], &link_type_str)?;
let duration = start.elapsed();
if ctx.options.verbose {
eprintln!(
"Conan build completed in {:.2}s: {}",
duration.as_secs_f64(),
sdk_archive.display()
);
}
Ok(BuildResult {
sdk_archive,
symbols_archive: None,
aar_archive: None,
duration_secs: duration.as_secs_f64(),
architectures: vec![host_arch],
})
}
fn clean(&self, ctx: &BuildContext) -> Result<()> {
for subdir in &["release", "debug"] {
let build_dir = ctx
.project_root
.join("cmake_build")
.join(subdir)
.join("conan");
if build_dir.exists() {
std::fs::remove_dir_all(&build_dir)
.with_context(|| format!("Failed to clean {}", build_dir.display()))?;
}
}
for old_dir in &[
ctx.project_root.join("cmake_build/Conan"),
ctx.project_root.join("cmake_build/conan"),
] {
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)
.with_context(|| format!("Failed to clean {}", old_dir.display()))?;
}
}
for old_dir in &[
ctx.project_root.join("target/release/conan"),
ctx.project_root.join("target/debug/conan"),
ctx.project_root.join("target/conan"),
] {
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)
.with_context(|| format!("Failed to clean {}", old_dir.display()))?;
}
}
let conan_dir = ctx.project_root.join("conan");
if conan_dir.exists() {
for subdir in &["build", "cmake-build-release", "cmake-build-debug"] {
let build_subdir = conan_dir.join(subdir);
if build_subdir.exists() {
std::fs::remove_dir_all(&build_subdir)
.with_context(|| format!("Failed to clean {}", build_subdir.display()))?;
}
}
}
Ok(())
}
}
impl Default for ConanBuilder {
fn default() -> Self {
Self::new()
}
}