pub mod gen_bindings;
pub mod gen_build_gradle;
pub mod gen_editorconfig;
pub mod gen_gitignore;
pub mod gen_gradle_properties;
pub mod gen_jni_skeleton;
pub mod gen_manifest;
pub mod gen_proguard;
pub mod gen_settings_gradle;
pub mod naming;
pub mod template_env;
pub mod trait_bridge;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::backends::kotlin::literal_normalizer;
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use crate::core::config::{KotlinFfiStyle, Language, ResolvedCrateConfig};
use crate::core::ir::{ApiSurface, TypeRef};
use crate::backends::kotlin_android::naming::package_path;
fn effective_exclude_types(config: &ResolvedCrateConfig) -> HashSet<String> {
let mut exclude_types: HashSet<String> = config
.ffi
.as_ref()
.map(|ffi| ffi.exclude_types.iter().cloned().collect())
.unwrap_or_default();
if let Some(ka) = &config.kotlin_android {
exclude_types.extend(ka.exclude_types.iter().cloned());
}
exclude_types
}
fn references_excluded_type(ty: &TypeRef, exclude_types: &HashSet<String>) -> bool {
exclude_types.iter().any(|name| ty.references_named(name))
}
fn signature_references_excluded_type(
params: &[crate::core::ir::ParamDef],
return_type: &TypeRef,
exclude_types: &HashSet<String>,
) -> bool {
references_excluded_type(return_type, exclude_types)
|| params
.iter()
.any(|param| references_excluded_type(¶m.ty, exclude_types))
}
fn api_without_excluded_types(api: &ApiSurface, exclude_types: &HashSet<String>) -> ApiSurface {
let mut filtered = api.clone();
filtered.types.retain(|typ| !exclude_types.contains(&typ.name));
for typ in &mut filtered.types {
typ.fields
.retain(|field| !references_excluded_type(&field.ty, exclude_types));
typ.methods
.retain(|method| !signature_references_excluded_type(&method.params, &method.return_type, exclude_types));
}
filtered
.enums
.retain(|enum_def| !exclude_types.contains(&enum_def.name));
for enum_def in &mut filtered.enums {
for variant in &mut enum_def.variants {
variant
.fields
.retain(|field| !references_excluded_type(&field.ty, exclude_types));
}
}
filtered
.functions
.retain(|func| !signature_references_excluded_type(&func.params, &func.return_type, exclude_types));
filtered.errors.retain(|error| !exclude_types.contains(&error.name));
filtered
}
const DEFAULT_AAR_ROOT: &str = "packages/kotlin-android";
const KOTLIN_SOURCE_INFIX: &str = "src/main/kotlin";
#[derive(Debug, Default, Clone, Copy)]
pub struct KotlinAndroidBackend;
impl Backend for KotlinAndroidBackend {
fn name(&self) -> &str {
"kotlin_android"
}
fn language(&self) -> Language {
Language::KotlinAndroid
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
supports_callbacks: false,
supports_streaming: true,
supports_service_api: false,
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let config = config.clone().with_kotlin_ffi_style(KotlinFfiStyle::Jni);
let config = &config;
let exclude_types = effective_exclude_types(config);
let filtered_api;
let api = if exclude_types.is_empty() {
api
} else {
filtered_api = api_without_excluded_types(api, &exclude_types);
&filtered_api
};
let layout = ProjectLayout::resolve(config);
let mut files = vec![
GeneratedFile {
path: layout.package_root.join("build.gradle.kts"),
content: gen_build_gradle::emit(config),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join("gradle.properties"),
content: gen_gradle_properties::emit(),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join("settings.gradle.kts"),
content: gen_settings_gradle::emit(config),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join("src/main/AndroidManifest.xml"),
content: gen_manifest::emit(config),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join("consumer-rules.pro"),
content: gen_proguard::emit_consumer(config),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join("proguard-rules.pro"),
content: gen_proguard::emit_module(),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join(".gitignore"),
content: gen_gitignore::emit(),
generated_header: false,
},
GeneratedFile {
path: layout.package_root.join(".editorconfig"),
content: gen_editorconfig::emit(),
generated_header: false,
},
];
files.extend(gen_jni_skeleton::emit(config, &layout.package_root));
files.extend(gen_bindings::emit(api, config, &layout.kotlin_source_dir));
apply_kotlin_post_processing(&mut files);
Ok(files)
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "gradle",
crate_suffix: "",
build_dep: BuildDependency::Ffi,
post_build: vec![],
})
}
}
#[derive(Debug, Clone)]
struct ProjectLayout {
package_root: PathBuf,
kotlin_source_dir: PathBuf,
}
impl ProjectLayout {
fn resolve(config: &ResolvedCrateConfig) -> Self {
let pkg_path = package_path(config);
match config.output_for("kotlin_android") {
Some(configured) => Self::from_configured(configured, &pkg_path),
None => Self::rooted_at(&PathBuf::from(DEFAULT_AAR_ROOT), &pkg_path),
}
}
fn from_configured(configured: &Path, pkg_path: &str) -> Self {
if let Some(package_root) = strip_kotlin_source_suffix(configured, pkg_path) {
Self {
package_root,
kotlin_source_dir: configured.to_path_buf(),
}
} else {
Self::rooted_at(configured, pkg_path)
}
}
fn rooted_at(package_root: &Path, pkg_path: &str) -> Self {
Self {
package_root: package_root.to_path_buf(),
kotlin_source_dir: package_root.join(KOTLIN_SOURCE_INFIX).join(pkg_path),
}
}
}
fn strip_kotlin_source_suffix(configured: &Path, pkg_path: &str) -> Option<PathBuf> {
let pkg_segment = PathBuf::from(pkg_path);
let pkg_components: Vec<_> = pkg_segment.components().collect();
let kotlin_components: Vec<_> = Path::new(KOTLIN_SOURCE_INFIX).components().collect();
let configured_components: Vec<_> = configured.components().collect();
let suffix_len = kotlin_components.len() + pkg_components.len();
if configured_components.len() < suffix_len {
return None;
}
let tail_start = configured_components.len() - suffix_len;
let tail = &configured_components[tail_start..];
let kotlin_matches = tail[..kotlin_components.len()]
.iter()
.zip(kotlin_components.iter())
.all(|(a, b)| a == b);
let pkg_matches = tail[kotlin_components.len()..]
.iter()
.zip(pkg_components.iter())
.all(|(a, b)| a == b);
if !(kotlin_matches && pkg_matches) {
return None;
}
let head = &configured_components[..tail_start];
if head.is_empty() {
return Some(PathBuf::from("."));
}
let mut root = PathBuf::new();
for comp in head {
root.push(comp);
}
Some(root)
}
fn apply_kotlin_post_processing(files: &mut [GeneratedFile]) {
for file in files {
if file.path.extension().is_some_and(|ext| ext == "kt") {
file.content = literal_normalizer::fix_float_literals(&file.content);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_kotlin_source_suffix_extracts_project_root() {
let configured = Path::new("packages/kotlin-android/src/main/kotlin/dev/sample_crate/sample_crawler/android");
let root = strip_kotlin_source_suffix(configured, "dev/sample_crate/sample_crawler/android");
assert_eq!(root, Some(PathBuf::from("packages/kotlin-android")));
}
#[test]
fn strip_kotlin_source_suffix_returns_none_when_suffix_missing() {
let configured = Path::new("packages/kotlin-android");
assert_eq!(strip_kotlin_source_suffix(configured, "dev/sample_crate"), None);
}
#[test]
fn from_configured_derives_package_root_when_path_targets_kotlin_source() {
let configured = Path::new("packages/kotlin-android/src/main/kotlin/dev/sample_crate/sample_crawler/android");
let layout = ProjectLayout::from_configured(configured, "dev/sample_crate/sample_crawler/android");
assert_eq!(layout.package_root, PathBuf::from("packages/kotlin-android"));
assert_eq!(layout.kotlin_source_dir, PathBuf::from(configured));
}
#[test]
fn from_configured_falls_back_to_legacy_when_path_is_project_root() {
let configured = Path::new("packages/kotlin-android");
let layout = ProjectLayout::from_configured(configured, "dev/sample_crate");
assert_eq!(layout.package_root, PathBuf::from("packages/kotlin-android"));
assert_eq!(
layout.kotlin_source_dir,
PathBuf::from("packages/kotlin-android/src/main/kotlin/dev/sample_crate")
);
}
#[test]
fn apply_kotlin_post_processing_fixes_double_literals_in_named_kt_files() {
let mut files = vec![GeneratedFile {
path: PathBuf::from("src/main/kotlin/dev/sample_crate/OcrQualityThresholds.kt"),
content: " val minNonWhitespacePerPage: Double = 32,\n".to_string(),
generated_header: true,
}];
apply_kotlin_post_processing(&mut files);
assert_eq!(files[0].content, " val minNonWhitespacePerPage: Double = 32.0,\n");
}
#[test]
fn apply_kotlin_post_processing_skips_non_kotlin_files() {
let mut files = vec![GeneratedFile {
path: PathBuf::from("build.gradle.kts"),
content: "ext = 32".to_string(),
generated_header: false,
}];
apply_kotlin_post_processing(&mut files);
assert_eq!(files[0].content, "ext = 32");
}
}