use std::path::PathBuf;
use std::time::Instant;
use anyhow::{bail, Context, Result};
use crate::build::archive::{
get_unified_include_path, ArchiveBuilder, ARCHIVE_DIR_FRAMEWORKS, ARCHIVE_DIR_SHARED,
ARCHIVE_DIR_STATIC,
};
use crate::build::cmake::{BuildType, CMakeConfig};
use crate::build::toolchains::xcode::{ApplePlatform, XcodeToolchain};
#[cfg(target_os = "macos")]
use crate::build::toolchains::Toolchain;
use crate::build::{BuildContext, BuildResult, PlatformBuilder};
use crate::commands::build::LinkType;
pub struct MacosBuilder {
xcode: Option<XcodeToolchain>,
}
impl MacosBuilder {
pub fn new() -> Self {
Self { xcode: None }
}
fn get_xcode(&mut self) -> Result<&XcodeToolchain> {
if self.xcode.is_none() {
self.xcode = Some(XcodeToolchain::detect()?);
}
Ok(self.xcode.as_ref().unwrap())
}
fn merge_module_static_libs(
&self,
xcode: &XcodeToolchain,
build_dir: &PathBuf,
lib_name: &str,
verbose: bool,
) -> Result<()> {
let out_dir = build_dir.join("out");
if !out_dir.exists() {
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 module_libs.is_empty() {
return Ok(());
}
let main_lib_name = format!("lib{}.a", lib_name);
if module_libs.len() == 1
&& module_libs[0]
.file_name()
.map_or(false, |n| n == main_lib_name.as_str())
{
return Ok(());
}
let main_lib_path = out_dir.join(&main_lib_name);
module_libs.retain(|p| p != &main_lib_path);
if module_libs.is_empty() {
return Ok(());
}
if verbose {
eprintln!(
" Merging {} module libraries into {}",
module_libs.len(),
main_lib_name
);
}
xcode.merge_static_libs(&module_libs, &main_lib_path)?;
for lib in &module_libs {
if lib != &main_lib_path {
let _ = std::fs::remove_file(lib);
}
}
Ok(())
}
fn build_arch(
&self,
ctx: &BuildContext,
xcode: &XcodeToolchain,
arch: &str,
link_type: &str,
) -> Result<PathBuf> {
let build_dir = ctx
.cmake_build_dir
.join(format!("{}/{}", link_type, arch));
let install_dir = build_dir.join("install");
let build_shared = link_type == "shared";
let cmake_vars = xcode.cmake_variables_for_platform(ApplePlatform::MacOS)?;
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())
.variable("CMAKE_OSX_ARCHITECTURES", arch)
.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());
}
for (name, value) in cmake_vars {
if name != "CMAKE_OSX_ARCHITECTURES" {
cmake = cmake.variable(&name, &value);
}
}
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(xcode, &build_dir, ctx.lib_name(), ctx.options.verbose)?;
self.merge_third_party_static_libs(xcode, &build_dir, ctx.lib_name(), ctx.options.verbose)?;
}
Ok(build_dir)
}
fn merge_third_party_static_libs(
&self,
xcode: &XcodeToolchain,
build_dir: &PathBuf,
lib_name: &str,
verbose: bool,
) -> Result<()> {
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 lib{}.a",
third_party_libs.len(),
lib_name
);
}
let mut all_libs = vec![main_lib_path.clone()];
all_libs.extend(third_party_libs);
xcode.merge_static_libs(&all_libs, &main_lib_path)?;
Ok(())
}
fn create_universal_binary(
&self,
xcode: &XcodeToolchain,
arch_libs: &[(String, PathBuf)], output: &PathBuf,
) -> Result<()> {
if arch_libs.len() == 1 {
std::fs::copy(&arch_libs[0].1, output)?;
return Ok(());
}
let lib_paths: Vec<PathBuf> = arch_libs.iter().map(|(_, p)| p.clone()).collect();
xcode.create_universal_binary(&lib_paths, output)?;
Ok(())
}
fn find_libraries(&self, install_dir: &PathBuf, is_shared: bool) -> Result<Vec<PathBuf>> {
let extension = if is_shared { "dylib" } else { "a" };
let mut libs = Vec::new();
let possible_dirs = vec![
install_dir.join("lib"),
install_dir.join("out"),
install_dir.to_path_buf(),
];
for lib_dir in possible_dirs {
if !lib_dir.exists() {
continue;
}
for entry in std::fs::read_dir(&lib_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == extension {
if !libs.iter().any(|p: &PathBuf| p.file_name() == path.file_name()) {
libs.push(path);
}
}
}
}
}
if !libs.is_empty() {
break;
}
}
Ok(libs)
}
fn build_link_type(
&mut self,
ctx: &BuildContext,
link_type: &str,
architectures: &[String],
) -> Result<PathBuf> {
let xcode = XcodeToolchain::detect()?;
if ctx.options.verbose {
eprintln!("Building {} library for macOS...", link_type);
}
let is_shared = link_type == "shared";
let mut arch_results: Vec<(String, PathBuf)> = Vec::new();
for arch in architectures {
if ctx.options.verbose {
eprintln!(" Building for {}...", arch);
}
let install_dir = self.build_arch(ctx, &xcode, arch, link_type)?;
arch_results.push((arch.clone(), install_dir));
}
let universal_dir = ctx.cmake_build_dir.join(format!("{}/universal", link_type));
let universal_lib_dir = universal_dir.join("lib");
std::fs::create_dir_all(&universal_lib_dir)?;
let first_install = &arch_results[0].1;
let libs = self.find_libraries(first_install, is_shared)?;
for lib in &libs {
let lib_name = lib.file_name().unwrap().to_str().unwrap();
let output_path = universal_lib_dir.join(lib_name);
let mut arch_libs: Vec<(String, PathBuf)> = Vec::new();
for (arch, install_dir) in &arch_results {
let possible_paths = vec![
install_dir.join("lib").join(lib_name),
install_dir.join("out").join(lib_name),
install_dir.join(lib_name),
];
for arch_lib in possible_paths {
if arch_lib.exists() {
arch_libs.push((arch.clone(), arch_lib));
break;
}
}
}
if !arch_libs.is_empty() {
self.create_universal_binary(&xcode, &arch_libs, &output_path)?;
}
}
let include_src = first_install.join("include");
let include_dst = universal_dir.join("include");
if include_src.exists() {
copy_dir_all(&include_src, &include_dst)?;
}
Ok(universal_dir)
}
fn find_lib_dir(&self, build_dir: &PathBuf) -> Option<PathBuf> {
let possible_dirs = vec![
build_dir.join("lib"),
build_dir.join("out"),
build_dir.to_path_buf(),
];
for dir in possible_dirs {
if dir.exists() && std::fs::read_dir(&dir).map(|d| d.count() > 0).unwrap_or(false) {
return Some(dir);
}
}
None
}
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 Xcode project for macOS in {}...",
build_dir.display()
);
let cc_output = Command::new("xcrun")
.args(["--find", "clang"])
.output()
.context("Failed to find clang via xcrun")?;
let cc = String::from_utf8_lossy(&cc_output.stdout).trim().to_string();
let cxx_output = Command::new("xcrun")
.args(["--find", "clang++"])
.output()
.context("Failed to find clang++ via xcrun")?;
let cxx = String::from_utf8_lossy(&cxx_output.stdout).trim().to_string();
let mut cmake_cmd = Command::new("cmake");
cmake_cmd
.arg("-S")
.arg(&ctx.project_root)
.arg("-B")
.arg(&build_dir)
.arg("-G")
.arg("Xcode")
.arg(format!("-DCMAKE_C_COMPILER={}", cc))
.arg(format!("-DCMAKE_CXX_COMPILER={}", cxx))
.arg("-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY")
.arg("-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=NO")
.arg("-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY=");
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 project_file = build_dir.join(format!("{}.xcodeproj", ctx.lib_name()));
if project_file.exists() {
eprintln!("\n✓ Xcode project generated: {}", project_file.display());
#[cfg(target_os = "macos")]
{
let _ = Command::new("open").arg(&project_file).status();
}
} else {
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 create_xcframework(
&self,
xcode: &XcodeToolchain,
universal_dir: &PathBuf,
output: &PathBuf,
is_shared: bool,
lib_name: &str,
) -> Result<()> {
if output.exists() {
std::fs::remove_dir_all(output)?;
}
let extension = if is_shared { "dylib" } else { "a" };
let main_lib_name = format!("lib{}.{}", lib_name, extension);
let lib_dir = self.find_lib_dir(universal_dir)
.ok_or_else(|| anyhow::anyhow!("Universal library directory not found"))?;
let main_lib = lib_dir.join(&main_lib_name);
if !main_lib.exists() {
let mut found = false;
for entry in std::fs::read_dir(&lib_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == extension {
xcode.create_xcframework(&[(path, None)], output)?;
found = true;
break;
}
}
}
}
if !found {
bail!("Main library {} not found in {}", main_lib_name, lib_dir.display());
}
} else {
xcode.create_xcframework(&[(main_lib, None)], output)?;
}
Ok(())
}
}
impl PlatformBuilder for MacosBuilder {
fn platform_name(&self) -> &str {
"macos"
}
fn default_architectures(&self) -> Vec<String> {
vec!["x86_64".to_string(), "arm64".to_string()]
}
fn validate_prerequisites(&self, _ctx: &BuildContext) -> Result<()> {
#[cfg(not(target_os = "macos"))]
{
bail!(
"macOS builds can only be run on macOS systems.\n\
Current OS: {}\n\n\
To build for macOS from your current OS, use Docker:\n \
ccgo build macos --docker",
std::env::consts::OS
);
}
#[cfg(target_os = "macos")]
{
if !crate::build::cmake::is_cmake_available() {
bail!("CMake is required for macOS builds. Please install CMake.");
}
let xcode = XcodeToolchain::detect()
.context("Xcode is required for macOS builds. Please install Xcode.")?;
xcode.validate()?;
if _ctx.options.verbose {
eprintln!(
"Using Xcode {} (build {})",
xcode.version(),
xcode.build_version()
);
}
Ok(())
}
}
fn build(&self, ctx: &BuildContext) -> Result<BuildResult> {
if ctx.options.ide_project {
return self.generate_ide_project(ctx);
}
let start = Instant::now();
let mut builder = MacosBuilder::new();
builder.validate_prerequisites(ctx)?;
if ctx.options.verbose {
eprintln!("Building {} for macOS...", ctx.lib_name());
}
let architectures = if ctx.options.architectures.is_empty() {
self.default_architectures()
} else {
ctx.options.architectures.clone()
};
std::fs::create_dir_all(&ctx.output_dir)?;
let archive = ArchiveBuilder::new(
ctx.lib_name(),
ctx.version(),
ctx.publish_suffix(),
ctx.options.release,
"macos",
ctx.output_dir.clone(),
)?;
let mut built_link_types = Vec::new();
if matches!(ctx.options.link_type, LinkType::Static | LinkType::Both) {
let universal_dir = builder.build_link_type(ctx, "static", &architectures)?;
let xcode = XcodeToolchain::detect()?;
let xcframework_path = ctx.cmake_build_dir.join("static/xcframework");
let xcframework = xcframework_path.join(format!("{}.xcframework", ctx.lib_name()));
builder.create_xcframework(&xcode, &universal_dir, &xcframework, false, ctx.lib_name())?;
if xcframework.exists() {
let archive_path = format!(
"{}/{}/{}/{}.xcframework",
ARCHIVE_DIR_FRAMEWORKS,
self.platform_name(),
ARCHIVE_DIR_STATIC,
ctx.lib_name()
);
archive.add_directory(&xcframework, &archive_path)?;
}
built_link_types.push("static");
}
if matches!(ctx.options.link_type, LinkType::Shared | LinkType::Both) {
let universal_dir = builder.build_link_type(ctx, "shared", &architectures)?;
let xcode = XcodeToolchain::detect()?;
let xcframework_path = ctx.cmake_build_dir.join("shared/xcframework");
let xcframework = xcframework_path.join(format!("{}.xcframework", ctx.lib_name()));
builder.create_xcframework(&xcode, &universal_dir, &xcframework, true, ctx.lib_name())?;
if xcframework.exists() {
let archive_path = format!(
"{}/{}/{}/{}.xcframework",
ARCHIVE_DIR_FRAMEWORKS,
self.platform_name(),
ARCHIVE_DIR_SHARED,
ctx.lib_name()
);
archive.add_directory(&xcframework, &archive_path)?;
}
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(&architectures, &link_type_str)?;
let duration = start.elapsed();
if ctx.options.verbose {
eprintln!(
"macOS 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,
})
}
fn clean(&self, ctx: &BuildContext) -> Result<()> {
for subdir in &["release", "debug"] {
let build_dir = ctx.project_root.join("cmake_build").join(subdir).join("macos");
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/macOS"),
ctx.project_root.join("cmake_build/macos"),
] {
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/macos"),
ctx.project_root.join("target/debug/macos"),
ctx.project_root.join("target/release/macOS"),
ctx.project_root.join("target/debug/macOS"),
ctx.project_root.join("target/macos"),
ctx.project_root.join("target/macOS"),
] {
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)
.with_context(|| format!("Failed to clean {}", old_dir.display()))?;
}
}
Ok(())
}
}
impl Default for MacosBuilder {
fn default() -> Self {
Self::new()
}
}
fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let dest_path = dst.join(entry.file_name());
if path.is_dir() {
copy_dir_all(&path, &dest_path)?;
} else {
std::fs::copy(&path, &dest_path)?;
}
}
Ok(())
}