use std::path::PathBuf;
use std::time::Instant;
use anyhow::{bail, Context, Result};
use crate::build::archive::{
get_unified_include_path, ArchiveBuilder, ARCHIVE_DIR_OBJ, ARCHIVE_DIR_SHARED,
ARCHIVE_DIR_STATIC,
};
use crate::build::cmake::{BuildType, CMakeConfig};
#[cfg(target_os = "linux")]
use crate::build::toolchains::detect_default_compiler;
use crate::build::{BuildContext, BuildResult, PlatformBuilder};
use crate::commands::build::LinkType;
pub struct LinuxBuilder;
impl LinuxBuilder {
pub fn new() -> Self {
Self
}
fn merge_module_static_libs(
&self,
build_dir: &PathBuf,
lib_name: &str,
verbose: bool,
) -> Result<()> {
use crate::build::toolchains::linux::LinuxToolchain;
let out_dir = build_dir.join("out");
if verbose {
eprintln!(" [merge] Checking out directory: {}", out_dir.display());
}
if !out_dir.exists() {
if verbose {
eprintln!(" [merge] Out directory does not exist, skipping merge");
}
return Ok(());
}
let main_lib_name = format!("lib{}.a", lib_name);
let main_lib_path = out_dir.join(&main_lib_name);
if main_lib_path.exists() {
if let Ok(metadata) = std::fs::metadata(&main_lib_path) {
if metadata.len() > 0 {
if verbose {
eprintln!(
" [merge] Main library {} already exists, skipping merge",
main_lib_name
);
}
for entry in std::fs::read_dir(&out_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path != main_lib_path {
if let Some(ext) = path.extension() {
if ext == "a" {
let _ = std::fs::remove_file(&path);
}
}
}
}
return Ok(());
}
}
}
let mut module_libs: Vec<PathBuf> = Vec::new();
for entry in std::fs::read_dir(&out_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "a" {
module_libs.push(path);
}
}
}
}
if verbose {
eprintln!(" [merge] Found {} .a files:", module_libs.len());
for lib in &module_libs {
eprintln!(" - {}", lib.file_name().unwrap().to_string_lossy());
}
}
if module_libs.is_empty() {
if verbose {
eprintln!(" [merge] No libraries found, skipping merge");
}
return Ok(());
}
if module_libs.len() == 1
&& module_libs[0]
.file_name()
.is_some_and(|n| n == main_lib_name.as_str())
{
if verbose {
eprintln!(" [merge] Only main library exists, skipping merge");
}
return Ok(());
}
module_libs.retain(|p| p != &main_lib_path);
if verbose {
eprintln!(
" [merge] Module libraries to merge: {}",
module_libs.len()
);
for lib in &module_libs {
eprintln!(" - {}", lib.file_name().unwrap().to_string_lossy());
}
}
if module_libs.is_empty() {
if verbose {
eprintln!(" [merge] No module libraries after filtering, skipping merge");
}
return Ok(());
}
eprintln!(
" Merging {} module libraries into {}",
module_libs.len(),
main_lib_name
);
let toolchain = LinuxToolchain::detect()?;
toolchain.merge_static_libs(&module_libs, &main_lib_path)?;
eprintln!(" Cleaning up {} module libraries...", module_libs.len());
for lib in &module_libs {
if lib != &main_lib_path {
match std::fs::remove_file(lib) {
Ok(_) => {
if verbose {
eprintln!(
" ✓ Removed: {}",
lib.file_name().unwrap().to_string_lossy()
);
}
}
Err(e) => {
eprintln!(
" âš Failed to remove {}: {}",
lib.file_name().unwrap().to_string_lossy(),
e
);
}
}
}
}
Ok(())
}
fn merge_third_party_static_libs(
&self,
build_dir: &PathBuf,
lib_name: &str,
verbose: bool,
) -> Result<()> {
use crate::build::toolchains::linux::LinuxToolchain;
let out_dir = build_dir.join("out");
let main_lib_path = out_dir.join(format!("lib{}.a", lib_name));
if !main_lib_path.exists() {
return Ok(());
}
let placeholder_name = format!("lib{}.a", lib_name);
let mut third_party_libs: Vec<PathBuf> = Vec::new();
for entry in std::fs::read_dir(build_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "a" {
let fname = path
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
if fname != placeholder_name {
third_party_libs.push(path);
}
}
}
}
}
if third_party_libs.is_empty() {
return Ok(());
}
if verbose {
eprintln!(
" Merging {} third-party libs into {}",
third_party_libs.len(),
placeholder_name
);
}
let toolchain = LinuxToolchain::detect()?;
let mut all_libs = vec![main_lib_path.clone()];
all_libs.extend(third_party_libs);
toolchain.merge_static_libs(&all_libs, &main_lib_path)?;
Ok(())
}
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");
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(';', ", ")
);
}
}
}
if let Some(cache) = ctx.compiler_cache() {
cmake = cmake.compiler_cache(cache);
}
cmake.configure_build_install()?;
if !build_shared {
self.merge_module_static_libs(&build_dir, ctx.lib_name(), ctx.options.verbose)?;
self.merge_third_party_static_libs(&build_dir, ctx.lib_name(), ctx.options.verbose)?;
}
Ok(build_dir)
}
pub fn generate_ide_project(&self, ctx: &BuildContext) -> Result<BuildResult> {
use std::process::Command;
let build_dir = ctx.cmake_build_dir.join("ide_project");
if build_dir.exists() {
std::fs::remove_dir_all(&build_dir)
.with_context(|| format!("Failed to clean {}", build_dir.display()))?;
}
std::fs::create_dir_all(&build_dir)
.with_context(|| format!("Failed to create {}", build_dir.display()))?;
eprintln!(
"Generating IDE project for Linux in {}...",
build_dir.display()
);
let mut cmake_cmd = Command::new("cmake");
cmake_cmd
.arg("-S")
.arg(&ctx.project_root)
.arg("-B")
.arg(&build_dir)
.arg("-G")
.arg("CodeLite - Unix Makefiles")
.arg("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON");
if let Some(cmake_dir) = ctx.ccgo_cmake_dir() {
cmake_cmd.arg(format!("-DCCGO_CMAKE_DIR={}", cmake_dir.display()));
}
cmake_cmd.arg(format!("-DCCGO_LIB_NAME={}", ctx.lib_name()));
if ctx.options.verbose {
eprintln!("CMake configure: {:?}", cmake_cmd);
}
let status = cmake_cmd
.status()
.context("Failed to run CMake configure")?;
if !status.success() {
bail!("CMake configure failed");
}
let workspace_file = build_dir.join(format!("{}.workspace", ctx.lib_name()));
let compile_commands = build_dir.join("compile_commands.json");
if workspace_file.exists() {
eprintln!(
"\n✓ CodeLite workspace generated: {}",
workspace_file.display()
);
}
if compile_commands.exists() {
eprintln!(
"✓ compile_commands.json generated: {}",
compile_commands.display()
);
eprintln!(
" (Use with VS Code C/C++ extension, CLion, or other LSP-compatible editors)"
);
let root_compile_commands = ctx.project_root.join("compile_commands.json");
if !root_compile_commands.exists() {
#[cfg(unix)]
{
let _ = std::os::unix::fs::symlink(&compile_commands, &root_compile_commands);
eprintln!(" Symlinked to project root for VS Code");
}
}
}
if !workspace_file.exists() && !compile_commands.exists() {
eprintln!(
"\n✓ IDE project files generated in: {}",
build_dir.display()
);
}
Ok(BuildResult {
sdk_archive: build_dir,
symbols_archive: None,
aar_archive: None,
duration_secs: 0.0,
architectures: vec![],
})
}
fn find_lib_dir(&self, build_dir: &PathBuf) -> Option<PathBuf> {
let possible_dirs = vec![
build_dir.join("out"), build_dir.join("install/lib"), build_dir.join("lib"),
];
for dir in possible_dirs {
if dir.exists() {
return Some(dir);
}
}
None
}
}
impl PlatformBuilder for LinuxBuilder {
fn platform_name(&self) -> &str {
"linux"
}
fn default_architectures(&self) -> Vec<String> {
vec!["x86_64".to_string()]
}
fn validate_prerequisites(&self, _ctx: &BuildContext) -> Result<()> {
#[cfg(not(target_os = "linux"))]
{
bail!(
"Linux builds can only be run on Linux systems.\n\
Current OS: {}\n\n\
To build for Linux from your current OS, use Docker:\n \
ccgo build linux --docker",
std::env::consts::OS
);
}
#[cfg(target_os = "linux")]
{
if !crate::build::cmake::is_cmake_available() {
bail!("CMake is required for Linux builds. Please install CMake.");
}
let compiler = detect_default_compiler()
.context("No C/C++ compiler found. Please install GCC or Clang.")?;
if _ctx.options.verbose {
eprintln!(
"Using {} compiler: {} ({})",
compiler.compiler_type,
compiler.cxx.display(),
compiler.version
);
}
Ok(())
}
}
fn build(&self, ctx: &BuildContext) -> Result<BuildResult> {
if ctx.options.ide_project {
return self.generate_ide_project(ctx);
}
let start = Instant::now();
self.validate_prerequisites(ctx)?;
if ctx.options.verbose {
eprintln!("Building {} for Linux...", ctx.lib_name());
}
ctx.materialize_source_deps(self.platform_name())?;
std::fs::create_dir_all(&ctx.output_dir)?;
let archive = ArchiveBuilder::new(
ctx.lib_name(),
ctx.version(),
ctx.publish_suffix(),
ctx.options.release,
"linux",
ctx.output_dir.clone(),
)?;
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) {
let archive_path = format!("lib/{}/{}", self.platform_name(), ARCHIVE_DIR_STATIC);
archive.add_directory_filtered(&lib_dir, &archive_path, &["a"])?;
}
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) {
let archive_path = format!("lib/{}/{}", self.platform_name(), ARCHIVE_DIR_SHARED);
archive.add_directory_filtered(&lib_dir, &archive_path, &["so", "a"])?;
}
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(&["x86_64".to_string()], &link_type_str)?;
let mut symbols_archive_result = None;
if ctx.options.link_type != LinkType::Static {
let symbols_temp =
std::env::temp_dir().join(format!("ccgo-symbols-{}", ctx.lib_name()));
std::fs::create_dir_all(&symbols_temp)?;
let obj_arch_dir = symbols_temp
.join(ARCHIVE_DIR_OBJ)
.join(self.platform_name())
.join("x86_64");
std::fs::create_dir_all(&obj_arch_dir)?;
let shared_build_dir = ctx.cmake_build_dir.join("shared");
if let Some(lib_dir) = self.find_lib_dir(&shared_build_dir) {
for entry in std::fs::read_dir(&lib_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "so") {
let file_name = path.file_name().unwrap();
std::fs::copy(&path, obj_arch_dir.join(file_name))?;
}
}
if obj_arch_dir.read_dir()?.next().is_some() {
let symbols_archive_path = archive.create_symbols_archive(&symbols_temp)?;
symbols_archive_result = Some(symbols_archive_path);
}
}
if symbols_temp.exists() {
std::fs::remove_dir_all(&symbols_temp)?;
}
}
let duration = start.elapsed();
if ctx.options.verbose {
eprintln!(
"Linux build completed in {:.2}s: {}",
duration.as_secs_f64(),
sdk_archive.display()
);
if let Some(ref sym) = symbols_archive_result {
eprintln!(" Symbols archive: {}", sym.display());
}
}
Ok(BuildResult {
sdk_archive,
symbols_archive: symbols_archive_result,
aar_archive: None,
duration_secs: duration.as_secs_f64(),
architectures: vec!["x86_64".to_string()],
})
}
fn clean(&self, ctx: &BuildContext) -> Result<()> {
for subdir in &["release", "debug"] {
let build_dir = ctx
.project_root
.join("cmake_build")
.join(subdir)
.join("linux");
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/Linux"),
ctx.project_root.join("cmake_build/linux"),
] {
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/linux"),
ctx.project_root.join("target/debug/linux"),
ctx.project_root.join("target/release/Linux"),
ctx.project_root.join("target/debug/Linux"),
ctx.project_root.join("target/linux"),
ctx.project_root.join("target/Linux"),
] {
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)
.with_context(|| format!("Failed to clean {}", old_dir.display()))?;
}
}
Ok(())
}
}
impl Default for LinuxBuilder {
fn default() -> Self {
Self::new()
}
}