use std::path::PathBuf;
use crate::cli::{CliError, Result};
use crate::commands::generate::{
run_generate_header_with_output_from_source_dir,
run_generate_kmp_with_output_from_source_dir_and_desktop_fallback_library_name,
};
use crate::commands::pack::PackKmpOptions;
use crate::config::{Config, Target};
use crate::pack::PackError;
use crate::pack::android::{AndroidBindingMode, AndroidPackager, build_android_targets};
use crate::pack::java::link::{
JvmNativePackageLayout, build_jvm_native_library, compile_jni_library_with_layout,
};
use crate::pack::java::outputs::remove_stale_structured_jvm_outputs;
use crate::pack::java::{
generate_jvm_header, prepare_kmp_jvm_packaging, selected_jvm_package_artifact_name,
selected_jvm_package_source_directory,
};
use crate::pack::{
discover_built_libraries_for_targets, missing_built_libraries, resolve_build_cargo_args,
};
use crate::reporter::Reporter;
use crate::target::Platform;
pub(crate) fn pack_kmp(
config: &Config,
options: PackKmpOptions,
reporter: &Reporter,
) -> Result<()> {
ensure_kmp_packaging_enabled(config, options.experimental)?;
ensure_kmp_no_build_supported(
config,
options.execution.no_build,
options.experimental,
"pack kmp",
)?;
reporter.section("🧩", "Packing Kotlin Multiplatform");
let step = reporter.step("Validating JVM toolchains");
let prepared_jvm_packaging = prepare_kmp_jvm_packaging(
config,
options.execution.release,
&options.execution.cargo_args,
)?;
step.finish_success();
let source_directory =
selected_jvm_package_source_directory(&prepared_jvm_packaging.packaging_targets)?;
let artifact_name =
selected_jvm_package_artifact_name(&prepared_jvm_packaging.packaging_targets)?;
let source_crate_name = config.library_name();
let jni_dir = kmp_jvm_jni_dir(config);
if options.execution.regenerate {
let step = reporter.step("Generating Kotlin Multiplatform bindings");
run_generate_kmp_with_output_from_source_dir_and_desktop_fallback_library_name(
config,
Some(config.kotlin_multiplatform_output()),
&source_directory,
source_crate_name,
artifact_name,
)?;
step.finish_success();
let step = reporter.step("Generating JVM C header");
generate_jvm_header(
&source_directory,
source_crate_name,
&jni_dir,
source_crate_name,
)?;
step.finish_success();
let step = reporter.step("Generating Android C header");
run_generate_header_with_output_from_source_dir(
config,
Some(config.android_header_output()),
&source_directory,
source_crate_name,
)?;
step.finish_success();
}
package_kmp_android_libraries(config, &options, reporter)?;
let kmp_jvm_layout = kmp_jvm_native_layout(config, source_crate_name);
for packaging_target in &prepared_jvm_packaging.packaging_targets {
let host_target = packaging_target.cargo_context.host_target;
let step = reporter.step(&format!(
"Building Rust library for {}",
host_target.canonical_name()
));
let build_artifacts =
build_jvm_native_library(packaging_target, options.execution.release, &step)?;
step.finish_success();
let step = reporter.step(&format!(
"Compiling JVM JNI library for {}",
host_target.canonical_name()
));
compile_jni_library_with_layout(
packaging_target,
&build_artifacts,
&kmp_jvm_layout,
&step,
)?;
step.finish_success();
}
remove_stale_structured_jvm_outputs(
&kmp_jvm_native_resource_root(config),
&prepared_jvm_packaging.host_targets,
)?;
reporter.finish();
Ok(())
}
fn ensure_kmp_packaging_enabled(config: &Config, experimental_flag: bool) -> Result<()> {
if !config.is_kotlin_multiplatform_enabled() {
return Err(CliError::CommandFailed {
command: "targets.kotlin_multiplatform.enabled = false".to_string(),
status: None,
});
}
if config.should_process(Target::KotlinMultiplatform, experimental_flag) {
return Ok(());
}
Err(CliError::CommandFailed {
command: format!(
"{} is experimental, use --experimental flag or add \"{}\" to [experimental]",
Target::KotlinMultiplatform.name(),
Target::KotlinMultiplatform.name()
),
status: None,
})
}
fn package_kmp_android_libraries(
config: &Config,
options: &PackKmpOptions,
reporter: &Reporter,
) -> Result<()> {
let build_cargo_args = resolve_build_cargo_args(config, &options.execution.cargo_args);
let build_profile =
crate::build::resolve_build_profile(options.execution.release, &build_cargo_args);
let android_targets = config.android_targets();
let step = reporter.step("Building Android targets for Kotlin Multiplatform");
build_android_targets(
config,
&android_targets,
options.execution.release,
&build_cargo_args,
&step,
)?;
step.finish_success();
let libraries = discover_built_libraries_for_targets(
&config.crate_artifact_name(),
build_profile.output_directory_name(),
&android_targets,
)?;
let android_libraries: Vec<_> = libraries
.into_iter()
.filter(|library| library.target.platform() == Platform::Android)
.collect();
let missing_targets = missing_built_libraries(&android_targets, &android_libraries);
if !missing_targets.is_empty() {
return Err(PackError::MissingBuiltLibraries {
platform: "Android".to_string(),
targets: missing_targets,
}
.into());
}
let packager = AndroidPackager::new(
config,
android_libraries,
build_profile.is_release_like(),
AndroidBindingMode::KotlinMultiplatform,
);
let step = reporter.step("Packaging Android jniLibs for Kotlin Multiplatform");
packager.package()?;
step.finish_success();
Ok(())
}
pub(crate) fn ensure_kmp_no_build_supported(
config: &Config,
no_build: bool,
experimental: bool,
command_name: &str,
) -> Result<()> {
if no_build && config.should_process(Target::KotlinMultiplatform, experimental) {
return Err(CliError::CommandFailed {
command: format!(
"{command_name} --no-build is unsupported while Kotlin Multiplatform native packaging is enabled; rerun without --no-build"
),
status: None,
});
}
Ok(())
}
fn kmp_jvm_jni_dir(config: &Config) -> PathBuf {
config.kotlin_multiplatform_output().join("src/jvmMain/c")
}
fn kmp_jvm_native_resource_root(config: &Config) -> PathBuf {
config
.kotlin_multiplatform_output()
.join("src/jvmMain/resources/native")
}
fn kmp_jvm_native_layout(config: &Config, header_name: &str) -> JvmNativePackageLayout {
JvmNativePackageLayout {
jni_dir: kmp_jvm_jni_dir(config),
header_name: header_name.to_string(),
jni_library_name: config.resolved_android_kotlin_desktop_library_name(),
native_output_root: kmp_jvm_native_resource_root(config),
flat_output_root: None,
strip_symbols: false,
debug_symbols_enabled: false,
}
}
#[cfg(test)]
mod tests {
use super::{
ensure_kmp_no_build_supported, ensure_kmp_packaging_enabled, kmp_jvm_jni_dir,
kmp_jvm_native_layout, kmp_jvm_native_resource_root,
};
use crate::cli::CliError;
use crate::config::Config;
use std::path::PathBuf;
fn parse_config(input: &str) -> Config {
let parsed: Config = toml::from_str(input).expect("toml parse failed");
parsed.validate().expect("config validation failed");
parsed
}
#[test]
fn kmp_jvm_paths_use_generated_kmp_project_layout() {
let config = parse_config(
r#"
experimental = ["kotlin_multiplatform"]
[package]
name = "demo-lib"
[targets.kotlin_multiplatform]
enabled = true
output = "dist/kmp"
module_name = "Demo"
"#,
);
let layout = kmp_jvm_native_layout(&config, "demo-lib");
assert_eq!(
kmp_jvm_jni_dir(&config),
PathBuf::from("dist/kmp/src/jvmMain/c")
);
assert_eq!(
kmp_jvm_native_resource_root(&config),
PathBuf::from("dist/kmp/src/jvmMain/resources/native")
);
assert_eq!(layout.jni_dir, PathBuf::from("dist/kmp/src/jvmMain/c"));
assert_eq!(layout.header_name, "demo-lib");
assert_eq!(layout.jni_library_name, "demo_lib");
assert_eq!(
layout.native_output_root,
PathBuf::from("dist/kmp/src/jvmMain/resources/native")
);
assert!(layout.flat_output_root.is_none());
assert!(!layout.strip_symbols);
assert!(!layout.debug_symbols_enabled);
}
#[test]
fn kmp_jvm_layout_uses_configured_kotlin_library_name_for_jni_output() {
let config = parse_config(
r#"
experimental = ["kotlin_multiplatform"]
[package]
name = "demo"
[targets.android.kotlin]
library_name = "configured-library"
[targets.kotlin_multiplatform]
enabled = true
output = "dist/kmp"
module_name = "Demo"
"#,
);
let layout = kmp_jvm_native_layout(&config, "demo");
assert_eq!(
kmp_jvm_jni_dir(&config),
PathBuf::from("dist/kmp/src/jvmMain/c")
);
assert_eq!(
kmp_jvm_native_resource_root(&config),
PathBuf::from("dist/kmp/src/jvmMain/resources/native")
);
assert_eq!(layout.jni_dir, PathBuf::from("dist/kmp/src/jvmMain/c"));
assert_eq!(layout.header_name, "demo");
assert_eq!(layout.jni_library_name, "configured_library");
assert_eq!(
layout.native_output_root,
PathBuf::from("dist/kmp/src/jvmMain/resources/native")
);
assert!(layout.flat_output_root.is_none());
assert!(!layout.strip_symbols);
assert!(!layout.debug_symbols_enabled);
}
#[test]
fn kmp_jvm_layout_does_not_inherit_java_strip_or_debug_symbols_policy() {
let config = parse_config(
r#"
experimental = ["kotlin_multiplatform"]
[package]
name = "demo"
[targets.kotlin_multiplatform]
enabled = true
output = "dist/kmp"
[targets.java.jvm]
strip_symbols = true
[targets.java.jvm.debug_symbols]
enabled = true
"#,
);
let layout = kmp_jvm_native_layout(&config, "demo");
assert!(config.java_jvm_strip_symbols());
assert!(config.java_jvm_debug_symbols_enabled());
assert!(layout.flat_output_root.is_none());
assert!(!layout.strip_symbols);
assert!(!layout.debug_symbols_enabled);
}
#[test]
fn kmp_packaging_requires_enabled_target() {
let config = parse_config(
r#"
[package]
name = "demo"
"#,
);
let error = ensure_kmp_packaging_enabled(&config, true).expect_err("target disabled");
assert!(
matches!(error, CliError::CommandFailed { command, .. } if command == "targets.kotlin_multiplatform.enabled = false")
);
}
#[test]
fn kmp_packaging_requires_experimental_opt_in() {
let config = parse_config(
r#"
[package]
name = "demo"
[targets.kotlin_multiplatform]
enabled = true
"#,
);
let error = ensure_kmp_packaging_enabled(&config, false).expect_err("missing opt-in");
assert!(
matches!(error, CliError::CommandFailed { command, .. } if command.contains("kotlin_multiplatform is experimental"))
);
}
#[test]
fn kmp_packaging_accepts_config_opt_in() {
let config = parse_config(
r#"
experimental = ["kotlin_multiplatform"]
[package]
name = "demo"
[targets.kotlin_multiplatform]
enabled = true
"#,
);
ensure_kmp_packaging_enabled(&config, false).expect("config opt-in");
}
#[test]
fn kmp_packaging_accepts_flag_opt_in() {
let config = parse_config(
r#"
[package]
name = "demo"
[targets.kotlin_multiplatform]
enabled = true
"#,
);
ensure_kmp_packaging_enabled(&config, true).expect("flag opt-in");
}
#[test]
fn rejects_no_build_when_kmp_packaging_is_enabled() {
let config = parse_config(
r#"
experimental = ["kotlin_multiplatform"]
[package]
name = "demo"
[targets.kotlin_multiplatform]
enabled = true
"#,
);
let error = ensure_kmp_no_build_supported(&config, true, false, "pack all")
.expect_err("expected no-build rejection");
assert!(matches!(
error,
CliError::CommandFailed { command, status: None }
if command.contains("pack all --no-build is unsupported")
&& command.contains("Kotlin Multiplatform native packaging")
));
}
#[test]
fn allows_no_build_when_kmp_packaging_is_not_selected() {
let config = parse_config(
r#"
[package]
name = "demo"
[targets.kotlin_multiplatform]
enabled = true
"#,
);
ensure_kmp_no_build_supported(&config, true, false, "pack all")
.expect("unselected KMP target should not reject no-build");
}
}