#![allow(dead_code)]
pub mod analytics;
pub mod archive;
pub mod cache;
pub mod cmake;
pub mod cmake_templates;
pub mod docker;
pub mod elf;
pub mod incremental;
pub mod linkage;
pub mod materialize;
pub mod platforms;
pub mod toolchains;
pub mod verinfo;
use std::collections::HashSet;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::commands::build::{BuildTarget, LinkType, WindowsToolchain};
use crate::config::CcgoConfig;
#[derive(Debug, Clone)]
pub struct BuildOptions {
pub target: BuildTarget,
pub architectures: Vec<String>,
pub link_type: LinkType,
pub use_docker: bool,
pub auto_docker: bool,
pub jobs: Option<usize>,
pub ide_project: bool,
pub release: bool,
pub native_only: bool,
pub toolchain: WindowsToolchain,
pub verbose: bool,
pub dev: bool,
pub features: Vec<String>,
pub use_default_features: bool,
pub all_features: bool,
pub cache: Option<String>,
pub analytics: bool,
pub linkage_default: Option<crate::config::Linkage>,
pub linkage_overrides: std::collections::HashMap<String, crate::config::Linkage>,
}
impl Default for BuildOptions {
fn default() -> Self {
Self {
target: BuildTarget::Linux,
architectures: Vec::new(),
link_type: LinkType::Both,
use_docker: false,
auto_docker: false,
jobs: None,
ide_project: false,
release: true,
native_only: false,
toolchain: WindowsToolchain::Auto,
verbose: false,
dev: false,
features: Vec::new(),
use_default_features: true,
all_features: false,
cache: None,
analytics: false,
linkage_default: None,
linkage_overrides: std::collections::HashMap::new(),
}
}
}
#[derive(Debug)]
pub struct BuildContext {
pub project_root: PathBuf,
pub config: CcgoConfig,
pub options: BuildOptions,
pub cmake_build_dir: PathBuf,
pub output_dir: PathBuf,
pub git_version: Option<crate::utils::git_version::GitVersion>,
}
impl BuildContext {
pub fn new(project_root: PathBuf, config: CcgoConfig, options: BuildOptions) -> Self {
let platform_name = options.target.to_string().to_lowercase();
let release_subdir = if options.release { "release" } else { "debug" };
let cmake_build_dir = project_root
.join("cmake_build")
.join(release_subdir)
.join(&platform_name);
let output_dir = project_root
.join("target")
.join(release_subdir)
.join(&platform_name);
let package = config
.package
.as_ref()
.expect("Build requires [package] section in CCGO.toml");
let git_version = crate::utils::git_version::GitVersion::from_project_root(
&project_root,
&package.version,
)
.ok();
if let Some(header_rel) = config
.build
.as_ref()
.and_then(|b| b.verinfo_path.as_deref())
{
if let Some(gv) = git_version.as_ref() {
let identity = gv.veridentity(&package.version);
let _ = verinfo::generate(&project_root, header_rel, &package.name, &identity);
}
}
Self {
project_root,
config,
options,
cmake_build_dir,
output_dir,
git_version,
}
}
pub fn lib_name(&self) -> &str {
&self
.config
.package
.as_ref()
.expect("Build requires [package] section")
.name
}
pub fn include_source_dir(&self) -> PathBuf {
if let Some(src) = self.config.include.as_ref().and_then(|c| c.src.as_deref()) {
self.project_root.join(src)
} else {
self.project_root.join("include")
}
}
pub fn version(&self) -> &str {
&self
.config
.package
.as_ref()
.expect("Build requires [package] section")
.version
}
pub fn publish_suffix(&self) -> &str {
""
}
pub fn version_with_suffix(&self) -> &str {
self.version()
}
pub fn jobs(&self) -> usize {
self.options.jobs.unwrap_or_else(num_cpus)
}
pub fn deps_map(&self) -> Option<String> {
let submodule_deps = self
.config
.build
.as_ref()
.map(|b| &b.submodule_deps)
.filter(|deps| !deps.is_empty())?;
let mut deps_list = Vec::new();
for (module, deps) in submodule_deps {
if !deps.is_empty() {
deps_list.push(module.clone());
deps_list.push(deps.join(","));
}
}
if deps_list.is_empty() {
None
} else {
Some(deps_list.join(";"))
}
}
pub fn symbol_visibility(&self) -> u8 {
if self
.config
.build
.as_ref()
.map(|b| b.symbol_visibility)
.unwrap_or(false)
{
1
} else {
0
}
}
pub fn resolved_features(&self) -> Result<HashSet<String>> {
let features_config = &self.config.features;
if self.options.all_features {
let mut all = HashSet::new();
for name in features_config.feature_names() {
features_config.resolve_feature(name, &mut all)?;
}
features_config.resolve_feature("default", &mut all)?;
Ok(all)
} else {
features_config
.resolve_features(&self.options.features, self.options.use_default_features)
}
}
pub fn cmake_feature_defines(&self) -> Result<String> {
let resolved = self.resolved_features()?;
let defines: Vec<String> = resolved
.iter()
.filter(|f| !f.contains('/')) .map(|f| format!("CCGO_FEATURE_{}", f.to_uppercase().replace('-', "_")))
.collect();
Ok(defines.join(";"))
}
pub fn enabled_dependencies(&self) -> Result<Vec<&crate::config::DependencyConfig>> {
let resolved = self.resolved_features()?;
Ok(self
.config
.features
.get_enabled_optional_deps(&resolved, &self.config.dependencies))
}
pub fn ccgo_cmake_dir(&self) -> Option<PathBuf> {
if let Ok(cmake_dir) = cmake_templates::get_cmake_dir() {
return Some(cmake_dir);
}
if let Ok(dir) = std::env::var("CCGO_CMAKE_DIR") {
let path = PathBuf::from(dir);
if path.exists() {
return Some(path);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(ccgo_rs_root) = exe
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
{
let cmake_dir = ccgo_rs_root
.parent()
.map(|p| p.join("ccgo/ccgo/build_scripts/cmake"));
if let Some(ref dir) = cmake_dir {
if dir.exists() {
return cmake_dir;
}
}
}
}
if let Ok(output) = std::process::Command::new("pip3")
.args(["show", "-f", "ccgo"])
.output()
{
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Some(location) = line.strip_prefix("Location: ") {
let cmake_dir =
PathBuf::from(location.trim()).join("ccgo/build_scripts/cmake");
if cmake_dir.exists() {
return Some(cmake_dir);
}
}
}
}
}
None
}
pub fn compiler_cache(&self) -> Option<cache::CacheConfig> {
let cache_option = self.options.cache.as_ref()?;
match cache_option.as_str() {
"none" | "disabled" | "off" => None,
"auto" => cache::CacheConfig::auto().ok(),
"ccache" => cache::CacheConfig::with_type(cache::CacheType::CCache).ok(),
"sccache" => cache::CacheConfig::with_type(cache::CacheType::SCache).ok(),
_ => {
eprintln!(
"⚠️ Unknown cache type '{}', using auto-detection",
cache_option
);
cache::CacheConfig::auto().ok()
}
}
}
pub fn config_hash(&self) -> Result<String> {
let config_path = self.project_root.join("CCGO.toml");
incremental::BuildState::hash_config(&config_path)
}
pub fn options_hash(&self) -> String {
let options_str = format!(
"target={:?},arch={:?},link={:?},release={},jobs={:?},features={:?},cache={:?}",
self.options.target,
self.options.architectures,
self.options.link_type,
self.options.release,
self.options.jobs,
self.options.features,
self.options.cache
);
incremental::BuildState::hash_options(&options_str)
}
pub fn resolved_dep_linkages(
&self,
platform: &str,
) -> anyhow::Result<Vec<(String, crate::build::linkage::ResolvedLinkage)>> {
use crate::build::linkage::{detect_dep_artifacts, resolve_linkage};
let platform = platform.to_lowercase();
let declared: std::collections::HashSet<&str> = self
.config
.dependencies
.iter()
.map(|d| d.name.as_str())
.collect();
for name in self.options.linkage_overrides.keys() {
if !declared.contains(name.as_str()) {
eprintln!(
"warning: --linkage {name}=<...> referenced an unknown dependency. \
'{name}' is not declared in [[dependencies]]; the override is ignored. \
Declared deps: {}",
if declared.is_empty() {
"(none)".to_string()
} else {
let mut names: Vec<&&str> = declared.iter().collect();
names.sort();
names
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(", ")
}
);
}
}
let consumer = self.options.link_type.preferred_single();
let mut out = Vec::new();
for dep in &self.config.dependencies {
let dep_root = self.project_root.join(".ccgo/deps").join(&dep.name);
let artifacts = detect_dep_artifacts(&dep_root, &platform);
let hint = self.resolved_linkage_hint(dep);
let resolved = resolve_linkage(consumer.clone(), artifacts, hint, &dep.name)?;
out.push((dep.name.clone(), resolved));
}
Ok(out)
}
fn resolved_linkage_hint(
&self,
dep: &crate::config::DependencyConfig,
) -> Option<crate::config::Linkage> {
let toml_default = self
.config
.build
.as_ref()
.and_then(|b| b.default_dep_linkage);
self.options
.linkage_overrides
.get(&dep.name)
.copied()
.or(self.options.linkage_default)
.or(dep.linkage)
.or(toml_default)
}
pub fn materialize_source_deps(&self, platform: &str) -> anyhow::Result<()> {
use crate::build::materialize::materialize_source_deps_inner;
let dep_hints: Vec<(String, Option<crate::config::Linkage>)> = self
.config
.dependencies
.iter()
.map(|d| (d.name.clone(), self.resolved_linkage_hint(d)))
.collect();
let archs: Vec<String> = self
.options
.architectures
.iter()
.map(|a| a.to_lowercase())
.collect();
let ccgo_bin =
std::env::current_exe().context("failed to resolve current ccgo binary path")?;
materialize_source_deps_inner(
&self.project_root,
platform,
&archs,
self.options.release,
&dep_hints,
ccgo_bin
.to_str()
.context("ccgo binary path is not UTF-8")?,
)
}
pub fn create_incremental_analyzer(
&self,
link_type: &str,
) -> Result<incremental::IncrementalAnalyzer> {
let config_hash = self.config_hash()?;
let options_hash = self.options_hash();
incremental::IncrementalAnalyzer::new(
&self.cmake_build_dir,
self.lib_name().to_string(),
self.options.target.to_string(),
link_type.to_string(),
config_hash,
options_hash,
)
}
}
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(4)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BuildInfo {
pub project: String,
pub platform: String,
pub version: String,
pub link_type: String,
pub build_time: String,
pub build_host: String,
pub architectures: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolchain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_branch: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BuildInfoFull {
pub build_metadata: BuildMetadata,
pub project: ProjectInfo,
pub git: GitInfo,
pub build: BuildDetails,
pub environment: EnvironmentInfo,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BuildMetadata {
pub version: String,
pub generated_at: String,
pub generator: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GitInfo {
pub branch: String,
pub revision: String,
pub revision_full: String,
pub tag: String,
pub is_dirty: bool,
pub remote_url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BuildDetails {
pub time: String,
pub timestamp: i64,
pub platform: String,
#[serde(flatten)]
pub platform_info: Option<PlatformBuildInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PlatformBuildInfo {
Ios { ios: IosBuildInfo },
Android { android: AndroidBuildInfo },
Macos { macos: MacosBuildInfo },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IosBuildInfo {
pub xcode_version: String,
pub xcode_build: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AndroidBuildInfo {
pub ndk_version: String,
pub stl: String,
pub min_sdk_version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MacosBuildInfo {
pub xcode_version: String,
pub xcode_build: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EnvironmentInfo {
pub os: String,
pub os_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub python_version: Option<String>,
pub ccgo_version: String,
}
pub trait PlatformBuilder {
fn platform_name(&self) -> &str;
fn default_architectures(&self) -> Vec<String>;
fn validate_prerequisites(&self, ctx: &BuildContext) -> Result<()>;
fn build(&self, ctx: &BuildContext) -> Result<BuildResult>;
fn clean(&self, ctx: &BuildContext) -> Result<()>;
}
#[derive(Debug)]
pub struct BuildResult {
pub sdk_archive: PathBuf,
pub symbols_archive: Option<PathBuf>,
pub aar_archive: Option<PathBuf>,
pub duration_secs: f64,
pub architectures: Vec<String>,
}