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};
use crate::build::toolchains::Toolchain;
use crate::build::{BuildContext, BuildResult, PlatformBuilder};
use crate::commands::build::LinkType;
pub struct TvosBuilder {
xcode: Option<XcodeToolchain>,
}
impl TvosBuilder {
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()
.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 build_arch(
&self,
ctx: &BuildContext,
xcode: &XcodeToolchain,
arch: &str,
link_type: &str,
sdk: &str,
) -> Result<PathBuf> {
let build_dir = ctx
.cmake_build_dir
.join(format!("{}/{}/{}", link_type, sdk, arch));
let install_dir = build_dir.join("install");
let build_shared = link_type == "shared";
let platform = if sdk == "simulator" {
ApplePlatform::TvOSSimulator
} else {
ApplePlatform::TvOS
};
let cmake_vars = xcode.cmake_variables_for_platform(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)
.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)?;
}
Ok(build_dir)
}
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 mut cmd = std::process::Command::new("xcodebuild");
cmd.arg("-create-xcframework");
let extension = if is_shared { "dylib" } else { "a" };
let main_lib_name = format!("lib{}.{}", lib_name, extension);
let device_lib_dir = device_lib.join("out");
let device_main_lib = device_lib_dir.join(&main_lib_name);
if device_main_lib.exists() {
cmd.arg("-library").arg(&device_main_lib);
} else {
bail!("Device library not found: {}", device_main_lib.display());
}
let sim_lib_dir = simulator_lib.join("out");
let sim_main_lib = sim_lib_dir.join(&main_lib_name);
if sim_main_lib.exists() {
cmd.arg("-library").arg(&sim_main_lib);
} else {
bail!("Simulator library not found: {}", sim_main_lib.display());
}
cmd.arg("-output").arg(output);
let status = cmd.status().context("Failed to run xcodebuild")?;
if !status.success() {
bail!("xcodebuild -create-xcframework failed");
}
Ok(())
}
fn build_link_type(
&mut self,
ctx: &BuildContext,
link_type: &str,
architectures: &[String],
) -> Result<(PathBuf, PathBuf)> {
let xcode = XcodeToolchain::detect()?;
if ctx.options.verbose {
eprintln!("Building {} library for tvOS...", link_type);
}
let device_archs: Vec<&str> = architectures
.iter()
.filter(|a| a.as_str() == "arm64")
.map(|s| s.as_str())
.collect();
let sim_archs: Vec<&str> = architectures
.iter()
.filter(|a| a.contains("simulator"))
.map(|s| s.as_str())
.collect();
if device_archs.is_empty() {
bail!("No device architectures specified for tvOS");
}
if sim_archs.is_empty() {
bail!("No simulator architectures specified for tvOS");
}
let device_dir = self.build_arch(ctx, &xcode, "arm64", link_type, "device")?;
let sim_arch = if sim_archs.contains(&"arm64-simulator") {
"arm64"
} else {
"x86_64"
};
let simulator_dir = self.build_arch(ctx, &xcode, sim_arch, link_type, "simulator")?;
Ok((device_dir, simulator_dir))
}
}
impl PlatformBuilder for TvosBuilder {
fn platform_name(&self) -> &str {
"tvos"
}
fn default_architectures(&self) -> Vec<String> {
vec!["arm64".to_string(), "arm64-simulator".to_string()]
}
fn validate_prerequisites(&self, ctx: &BuildContext) -> Result<()> {
if !crate::build::cmake::is_cmake_available() {
bail!("CMake is required for tvOS builds. Please install CMake.");
}
let xcode = XcodeToolchain::detect()
.context("Xcode is required for tvOS 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> {
let start = Instant::now();
let mut builder = TvosBuilder::new();
builder.validate_prerequisites(ctx)?;
if ctx.options.verbose {
eprintln!("Building {} for tvOS...", ctx.lib_name());
}
ctx.materialize_source_deps(self.platform_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,
"tvos",
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) = builder.build_link_type(ctx, "static", &architectures)?;
let xcframework_path = ctx.cmake_build_dir.join("static/xcframework");
let xcframework = xcframework_path.join(format!("{}.xcframework", ctx.lib_name()));
builder.create_xcframework(
&XcodeToolchain::detect()?,
&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) = builder.build_link_type(ctx, "shared", &architectures)?;
let xcframework_path = ctx.cmake_build_dir.join("shared/xcframework");
let xcframework = xcframework_path.join(format!("{}.xcframework", ctx.lib_name()));
builder.create_xcframework(
&XcodeToolchain::detect()?,
&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 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!(
"tvOS 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("tvos");
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/tvOS"),
ctx.project_root.join("cmake_build/tvos"),
] {
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/tvos"),
ctx.project_root.join("target/debug/tvos"),
ctx.project_root.join("target/release/tvOS"),
ctx.project_root.join("target/debug/tvOS"),
ctx.project_root.join("target/tvos"),
ctx.project_root.join("target/tvOS"),
] {
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)
.with_context(|| format!("Failed to clean {}", old_dir.display()))?;
}
}
Ok(())
}
}
impl Default for TvosBuilder {
fn default() -> Self {
Self::new()
}
}