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;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IosTarget {
Device,
Simulator,
}
impl IosTarget {
fn platform(&self) -> ApplePlatform {
match self {
IosTarget::Device => ApplePlatform::IOS,
IosTarget::Simulator => ApplePlatform::IOSSimulator,
}
}
fn name(&self) -> &str {
match self {
IosTarget::Device => "device",
IosTarget::Simulator => "simulator",
}
}
}
pub struct IosBuilder;
impl IosBuilder {
pub fn new() -> Self {
Self
}
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()
.is_some_and(|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 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 {}",
third_party_libs.len(),
placeholder_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 build_target_arch(
&self,
ctx: &BuildContext,
xcode: &XcodeToolchain,
target: IosTarget,
arch: &str,
link_type: &str,
) -> Result<PathBuf> {
let build_dir =
ctx.cmake_build_dir
.join(format!("{}/{}/{}", link_type, target.name(), arch));
let install_dir = build_dir.join("install");
let build_shared = link_type == "shared";
let cmake_vars = xcode.cmake_variables_for_platform(target.platform())?;
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)
.variable("CMAKE_SYSTEM_NAME", "iOS")
.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 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 create_universal_binary(
&self,
xcode: &XcodeToolchain,
arch_libs: &[PathBuf],
output: &PathBuf,
) -> Result<()> {
if arch_libs.len() == 1 {
std::fs::copy(&arch_libs[0], output)?;
return Ok(());
}
xcode.create_universal_binary(arch_libs, output)?;
Ok(())
}
fn build_link_type(
&self,
ctx: &BuildContext,
xcode: &XcodeToolchain,
link_type: &str,
) -> Result<(PathBuf, PathBuf)> {
if ctx.options.verbose {
eprintln!("Building {} library for iOS...", link_type);
}
let is_shared = link_type == "shared";
if ctx.options.verbose {
eprintln!(" Building for device (arm64)...");
}
let device_install =
self.build_target_arch(ctx, xcode, IosTarget::Device, "arm64", link_type)?;
let sim_archs = vec!["arm64", "x86_64"];
let mut sim_arch_installs: Vec<(String, PathBuf)> = Vec::new();
for arch in &sim_archs {
if ctx.options.verbose {
eprintln!(" Building for simulator ({})...", arch);
}
let install =
self.build_target_arch(ctx, xcode, IosTarget::Simulator, arch, link_type)?;
sim_arch_installs.push((arch.to_string(), install));
}
let sim_universal_dir = ctx
.cmake_build_dir
.join(format!("{}/simulator-universal", link_type));
let sim_lib_dir = sim_universal_dir.join("lib");
std::fs::create_dir_all(&sim_lib_dir)?;
let first_sim_install = &sim_arch_installs[0].1;
let libs = self.find_libraries(first_sim_install, is_shared)?;
for lib in &libs {
let lib_name = lib.file_name().unwrap().to_str().unwrap();
let output_path = sim_lib_dir.join(lib_name);
let mut arch_libs: Vec<PathBuf> = Vec::new();
for (_, install_dir) in &sim_arch_installs {
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_lib);
break;
}
}
}
if !arch_libs.is_empty() {
self.create_universal_binary(xcode, &arch_libs, &output_path)?;
}
}
let include_src = first_sim_install.join("include");
let include_dst = sim_universal_dir.join("include");
if include_src.exists() {
copy_dir_all(&include_src, &include_dst)?;
}
Ok((device_install, sim_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
}
fn create_xcframework(
&self,
xcode: &XcodeToolchain,
device_lib: &PathBuf,
simulator_lib: &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 device_lib_dir = self
.find_lib_dir(device_lib)
.ok_or_else(|| anyhow::anyhow!("Device library directory not found"))?;
let sim_lib_dir = self
.find_lib_dir(simulator_lib)
.ok_or_else(|| anyhow::anyhow!("Simulator library directory not found"))?;
let mut inputs: Vec<(PathBuf, Option<PathBuf>)> = Vec::new();
let device_main_lib = device_lib_dir.join(&main_lib_name);
if device_main_lib.exists() {
inputs.push((device_main_lib, None));
} else {
for entry in std::fs::read_dir(&device_lib_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == extension {
inputs.push((path, None));
break;
}
}
}
}
}
let sim_main_lib = sim_lib_dir.join(&main_lib_name);
if sim_main_lib.exists() {
inputs.push((sim_main_lib, None));
} else {
for entry in std::fs::read_dir(&sim_lib_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == extension {
inputs.push((path, None));
break;
}
}
}
}
}
if inputs.is_empty() {
bail!("No libraries found to create XCFramework");
}
xcode.create_xcframework(&inputs, output)?;
Ok(())
}
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 iOS in {}...",
build_dir.display()
);
let xcode = XcodeToolchain::detect()?;
let cmake_vars = xcode.cmake_variables_for_platform(ApplePlatform::IOS)?;
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("-DCMAKE_SYSTEM_NAME=iOS");
if let Some(cmake_dir) = ctx.ccgo_cmake_dir() {
cmake_cmd.arg(format!("-DCCGO_CMAKE_DIR={}", cmake_dir.display()));
}
for (name, value) in cmake_vars {
cmake_cmd.arg(format!("-D{}={}", name, value));
}
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![],
})
}
}
impl PlatformBuilder for IosBuilder {
fn platform_name(&self) -> &str {
"ios"
}
fn default_architectures(&self) -> Vec<String> {
vec![
"arm64".to_string(), "arm64-simulator".to_string(), "x86_64-simulator".to_string(), ]
}
fn validate_prerequisites(&self, _ctx: &BuildContext) -> Result<()> {
#[cfg(not(target_os = "macos"))]
{
bail!(
"iOS builds can only be run on macOS systems.\n\
Current OS: {}\n\n\
To build for iOS from your current OS, use Docker:\n \
ccgo build ios --docker",
std::env::consts::OS
);
}
#[cfg(target_os = "macos")]
{
if !crate::build::cmake::is_cmake_available() {
bail!("CMake is required for iOS builds. Please install CMake.");
}
let xcode = XcodeToolchain::detect()
.context("Xcode is required for iOS builds. Please install Xcode.")?;
xcode.validate()?;
xcode.sdk_path(ApplePlatform::IOS)?;
xcode.sdk_path(ApplePlatform::IOSSimulator)?;
if _ctx.options.verbose {
eprintln!(
"Using Xcode {} (build {})",
xcode.version(),
xcode.build_version()
);
eprintln!(" iOS SDK: {}", xcode.sdk_version(ApplePlatform::IOS)?);
eprintln!(
" iOS Simulator SDK: {}",
xcode.sdk_version(ApplePlatform::IOSSimulator)?
);
}
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)?;
let xcode = XcodeToolchain::detect()?;
if ctx.options.verbose {
eprintln!("Building {} for iOS...", 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,
"ios",
ctx.output_dir.clone(),
)?;
let mut built_link_types = Vec::new();
if matches!(ctx.options.link_type, LinkType::Static | LinkType::Both) {
let (device_dir, sim_dir) = self.build_link_type(ctx, &xcode, "static")?;
let xcframework_path = ctx.cmake_build_dir.join("static/xcframework");
let xcframework = xcframework_path.join(format!("{}.xcframework", ctx.lib_name()));
self.create_xcframework(
&xcode,
&device_dir,
&sim_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 (device_dir, sim_dir) = self.build_link_type(ctx, &xcode, "shared")?;
let xcframework_path = ctx.cmake_build_dir.join("shared/xcframework");
let xcframework = xcframework_path.join(format!("{}.xcframework", ctx.lib_name()));
self.create_xcframework(
&xcode,
&device_dir,
&sim_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 architectures = vec![
"arm64".to_string(),
"arm64-simulator".to_string(),
"x86_64-simulator".to_string(),
];
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!(
"iOS 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("ios");
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/iOS"),
ctx.project_root.join("cmake_build/ios"),
] {
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/ios"),
ctx.project_root.join("target/debug/ios"),
ctx.project_root.join("target/release/iOS"),
ctx.project_root.join("target/debug/iOS"),
ctx.project_root.join("target/ios"),
ctx.project_root.join("target/iOS"),
] {
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)
.with_context(|| format!("Failed to clean {}", old_dir.display()))?;
}
}
Ok(())
}
}
impl Default for IosBuilder {
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(())
}