use crate::core::backend::GeneratedFile;
use crate::core::config::{FfiTargetDepOverride, Language, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use crate::core::template_versions as tv;
use std::path::PathBuf;
fn render_jni_target_blocks(
crate_name: &str,
rel_path: &str,
default_features: &str,
version: &str,
overrides: &[FfiTargetDepOverride],
) -> String {
if overrides.is_empty() {
return String::new();
}
let cfgs: Vec<&str> = overrides.iter().map(|o| o.cfg.as_str()).collect();
let combined_cfg = if cfgs.len() == 1 {
cfgs[0].to_owned()
} else {
format!("any({})", cfgs.join(", "))
};
let mut entries: Vec<(String, String)> = vec![(
format!("not({combined_cfg})"),
crate::scaffold::render_core_dep(crate_name, rel_path, default_features, version),
)];
for override_ in overrides {
let features_str = if override_.features.is_empty() {
String::new()
} else {
let quoted: Vec<String> = override_.features.iter().map(|f| format!("\"{f}\"")).collect();
format!(", features = [{}]", quoted.join(", "))
};
entries.push((
override_.cfg.clone(),
crate::scaffold::render_core_dep(crate_name, rel_path, &features_str, version),
));
}
let joined = crate::scaffold::join_sorted_target_dep_blocks(entries);
format!("\n{joined}")
}
pub(crate) fn scaffold_jni(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let jni_crate_name = format!("{}-jni", config.jni_crate_base());
let jni_lib_name = config.jni_lib_name();
let features_str = crate::scaffold::core_dep_features(config, Language::KotlinAndroid);
let umbrella_dep_name = &config.name;
let target_overrides = config
.jni
.as_ref()
.map(|c| c.target_dep_overrides.as_slice())
.unwrap_or(&[]);
let rel_path = config.core_crate_dep_path(std::path::Path::new(&format!("crates/{jni_crate_name}")));
let mut dep_lines: Vec<String> = vec![
crate::scaffold::render_workspace_dep_or(config, "async-trait", &format!("\"{}\"", tv::cargo::ASYNC_TRAIT)),
crate::scaffold::render_workspace_dep_or(config, "base64", &format!("\"{}\"", tv::cargo::BASE64)),
crate::scaffold::render_workspace_dep_or(config, "futures-util", &format!("\"{}\"", tv::cargo::FUTURES_UTIL)),
crate::scaffold::render_workspace_dep_or(config, "jni", &format!("\"{}\"", tv::cargo::JNI)),
crate::scaffold::render_workspace_dep_or(config, "serde_json", &format!("\"{}\"", tv::cargo::SERDE_JSON)),
crate::scaffold::render_workspace_dep_or(
config,
"tokio",
"{ version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"sync\"] }",
),
crate::scaffold::render_workspace_dep_or(config, "tracing", &format!("\"{}\"", tv::cargo::TRACING)),
];
if target_overrides.is_empty() {
dep_lines.push(crate::scaffold::render_core_dep(
umbrella_dep_name,
&rel_path,
&features_str,
&api.version,
));
}
crate::scaffold::sort_dependency_lines(&mut dep_lines);
let deps_section = dep_lines.join("\n");
let target_blocks_section = render_jni_target_blocks(
umbrella_dep_name,
&rel_path,
&features_str,
&api.version,
target_overrides,
);
let lints_section = crate::scaffold::cargo_lints_section(config);
let crate_dir = format!("crates/{jni_crate_name}");
let ws = crate::scaffold::detect_workspace_inheritance_for_crate(config.workspace_root.as_deref(), &crate_dir);
let package_header = crate::scaffold::cargo_package_header(
&jni_crate_name,
&api.version,
"2024",
&crate::scaffold::scaffold_meta(config),
&ws,
);
let content = format!(
r#"# Generated by alef. Do not edit by hand.
{package_header}
# `base64`, `futures-util`, `serde_json`, and `tokio` are emitted unconditionally below
# so the manifest is stable across regens (they are used when the umbrella
# crate declares binary top-level params, async fns, streaming adapters, or JSON-marshalled types),
# but for an umbrella crate that has none of those they are genuinely unused.
# List them here so `cargo machete` doesn't flag the no-async-no-streaming
# case as a real finding.
[package.metadata.cargo-machete]
ignored = ["async-trait", "base64", "futures-util", "serde_json", "tokio", "tracing"]
[lib]
name = "{jni_lib_name}"
crate-type = ["cdylib"]
[dependencies]
{deps_section}
{target_blocks_section}{lints_section}"#,
package_header = package_header,
jni_lib_name = jni_lib_name,
lints_section = lints_section,
deps_section = deps_section,
target_blocks_section = target_blocks_section,
);
let _ = api;
Ok(vec![GeneratedFile {
path: PathBuf::from(format!("crates/{jni_crate_name}/Cargo.toml")),
content,
generated_header: true,
}])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
use crate::core::ir::ApiSurface;
fn resolved_one(toml: &str) -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
cfg.resolve().unwrap().remove(0)
}
#[test]
fn scaffold_jni_cargo_toml_writes_explicit_package_fields_without_a_workspace() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
let cargo_toml = &scaffold_jni(&ApiSurface::default(), &config).unwrap()[0].content;
assert!(
cargo_toml.contains("edition = \"2024\""),
"without a workspace root the edition must be written explicitly; got:\n{cargo_toml}"
);
assert!(
!cargo_toml.contains(".workspace = true"),
"no [package] field may claim workspace inheritance when no workspace is reachable; got:\n{cargo_toml}"
);
let expected = config.core_crate_dep_path(std::path::Path::new("crates/demo-llm-jni"));
assert_eq!(expected, "../..", "sanity: root-flat core crate sits two levels up");
assert!(
cargo_toml.contains(&format!(r#"path = "{expected}""#)),
"the core dep path must be derived from the emitted layout; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_cargo_toml_declares_tracing_dependency() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
cargo_toml.contains(&format!(
"tracing = \"{}\"",
crate::core::template_versions::cargo::TRACING
)),
"JNI Cargo.toml must declare the tracing dependency; got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains("\"tracing\""),
"JNI Cargo.toml must ignore tracing in cargo-machete since it may be unused until trait bridges are configured; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_cargo_toml_emits_configured_cargo_lints() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
[crates.cargo_lints.rust]
unused_must_use = "deny"
[crates.cargo_lints.clippy]
unwrap_used = "warn"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
cargo_toml.contains("[lints.rust]\nunused_must_use = \"deny\""),
"expected [lints.rust] block, got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains(
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\n\
print_stdout = \"deny\"\nunwrap_used = \"warn\""
),
"expected the configured clippy entry to merge with the builtin deny defaults, got:\n{cargo_toml}"
);
toml::from_str::<toml::Value>(cargo_toml).expect("generated Cargo.toml with cargo_lints must be valid TOML");
}
#[test]
fn scaffold_jni_cargo_toml_emits_builtin_clippy_denies_when_cargo_lints_unset() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
!cargo_toml.contains("[lints.rust]"),
"no [lints.rust] table should be emitted when cargo_lints.rust is unset, got:\n{cargo_toml}"
);
assert!(
cargo_toml
.contains("[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"deny\""),
"the builtin [lints.clippy] deny block must survive even when cargo_lints is unset, got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_inherits_declared_workspace_dependencies() {
let workspace = tempfile::tempdir().expect("create workspace");
std::fs::write(
workspace.path().join("Cargo.toml"),
r#"
[workspace]
members = []
[workspace.dependencies]
base64 = "0.22"
jni = "0.22"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
"#,
)
.expect("write workspace manifest");
let mut config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
config.workspace_root = Some(workspace.path().to_path_buf());
let files = scaffold_jni(&ApiSurface::default(), &config).expect("scaffold JNI");
let cargo_toml = &files[0].content;
assert!(cargo_toml.contains("base64.workspace = true"));
assert!(cargo_toml.contains("jni.workspace = true"));
assert!(cargo_toml.contains("tokio.workspace = true"));
assert!(cargo_toml.contains("async-trait = \"0.1\""));
assert!(!cargo_toml.contains("base64 = \"0.22\""));
}
#[test]
fn scaffold_jni_lib_name_uses_ffi_prefix() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "demoffi"
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
cargo_toml.contains("name = \"demoffi_jni\""),
"expected `name = \"demoffi_jni\"` but got:\n{cargo_toml}"
);
assert!(
!cargo_toml.contains("name = \"demo_llm_jni\""),
"cdylib name must not fall back to snake-cased crate name when prefix is set; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_path_uses_config_name_not_core_crate_dir() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "sample-language-pack"
sources = ["crates/sample-pack-core/src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.sample_language_pack.android"
namespace = "dev.sample_crate.sample_language_pack.android"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
assert_eq!(files.len(), 1);
let path = files[0].path.to_string_lossy();
let cargo_toml = &files[0].content;
assert!(files[0].generated_header, "generated JNI manifest must be Alef-owned");
assert_eq!(
path, "crates/sample-language-pack-jni/Cargo.toml",
"JNI scaffold path must follow config.name, not core_crate_dir; got: {path}"
);
assert!(
cargo_toml.contains("name = \"sample-language-pack-jni\""),
"[package] name must follow config.name; got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains("sample-language-pack = { path = \"../sample-pack-core\""),
"umbrella dep key must be cargo package name with path = ../<core_crate_dir>; got:\n{cargo_toml}"
);
assert!(
!cargo_toml.contains("sample-pack-core = { path = \"../sample-pack-core\""),
"umbrella dep key must NOT be the directory name; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_lib_name_defaults_to_snake_case_crate_name() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "plain-pkg"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.plain"
namespace = "dev.sample_crate.plain"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
cargo_toml.contains("name = \"plain_pkg_jni\""),
"expected `name = \"plain_pkg_jni\"` for default case; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_crate_dir_override_controls_output_path() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-render-rs"
sources = ["crates/demo-render/src/lib.rs"]
[crates.jni]
crate_dir = "demo-render"
[crates.kotlin_android]
package = "dev.example.demo_render.android"
namespace = "dev.example.demo_render.android"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
assert_eq!(files.len(), 1);
let path = files[0].path.to_string_lossy();
let cargo_toml = &files[0].content;
assert_eq!(
path, "crates/demo-render-jni/Cargo.toml",
"JNI scaffold path must follow [crates.jni] crate_dir override; got: {path}"
);
assert!(
cargo_toml.contains("name = \"demo-render-jni\""),
"[package] name must follow crate_dir override; got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains("demo-render-rs = { path = \"../demo-render\""),
"umbrella dep key must be cargo package name, path must be core_crate_dir; got:\n{cargo_toml}"
);
assert!(
!cargo_toml.contains("demo-render = { path = \"../demo-render\""),
"umbrella dep key must NOT be the crate_dir override; got:\n{cargo_toml}"
);
assert!(
!cargo_toml.contains("demo-render-rs-jni"),
"crate name must NOT contain the -rs suffix; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_dependency_keys_are_in_cargo_sort_order() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "sample_stream"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.example.sample_stream"
namespace = "dev.example.sample_stream"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
crate::test_support::cargo_sort_order::assert_dependency_keys_sorted("jni Cargo.toml", cargo_toml) > 0,
"the JNI manifest must carry dependency keys to compare:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_emits_target_dep_overrides() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-doc"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.example.demo"
namespace = "dev.example.demo"
features = ["full"]
[[crates.jni.target_dep_overrides]]
cfg = 'target_os = "android"'
features = ["android-target"]
[[crates.jni.target_dep_overrides]]
cfg = 'target_os = "ios"'
features = ["android-target"]
[[crates.jni.target_dep_overrides]]
cfg = 'target_os = "windows"'
features = ["windows-target"]
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
let deps_start = cargo_toml.find("[dependencies]").expect("missing [dependencies]");
let first_target = cargo_toml
.find("[target.")
.expect("expected at least one [target.*] block");
let inline_deps = &cargo_toml[deps_start..first_target];
assert!(
!inline_deps.contains("demo-doc ="),
"core-crate dep must not appear inline when overrides are present; got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains(
r#"[target.'cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))'.dependencies]"#
),
"default branch must be gated on cfg(not(any(...))); got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains(r#"[target.'cfg(target_os = "android")'.dependencies]"#)
&& cargo_toml.contains(r#"features = ["android-target"]"#),
"android override block must carry android-target; got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains(r#"[target.'cfg(target_os = "windows")'.dependencies]"#)
&& cargo_toml.contains(r#"features = ["windows-target"]"#),
"windows override block must carry windows-target; got:\n{cargo_toml}"
);
let core_dep_lines = cargo_toml.matches("demo-doc = {").count();
assert_eq!(
core_dep_lines, 4,
"expected one core-dep line per target branch (default + 3 overrides); got {core_dep_lines}:\n{cargo_toml}"
);
toml::from_str::<toml::Value>(cargo_toml).expect("generated JNI Cargo.toml must be valid TOML");
}
#[test]
fn scaffold_jni_target_dep_overrides_sort_all_before_not() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-doc"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.example.demo"
namespace = "dev.example.demo"
features = ["full"]
[[crates.jni.target_dep_overrides]]
cfg = 'target_os = "android"'
features = ["android-target"]
[[crates.jni.target_dep_overrides]]
cfg = 'target_os = "ios"'
features = ["android-target"]
[[crates.jni.target_dep_overrides]]
cfg = 'target_os = "windows"'
features = ["windows-target"]
[[crates.jni.target_dep_overrides]]
cfg = 'all(target_os = "macos", target_arch = "x86_64")'
features = ["macos-intel-target"]
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
let all_pos = cargo_toml
.find(r#"[target.'cfg(all(target_os = "macos", target_arch = "x86_64"))'.dependencies]"#)
.expect("expected the macOS-Intel `all(...)` override block");
let not_pos = cargo_toml
.find("[target.'cfg(not(any(")
.expect("expected the default `not(any(...))` block");
let android_pos = cargo_toml
.find(r#"[target.'cfg(target_os = "android")'.dependencies]"#)
.expect("expected the android override block");
assert!(
all_pos < not_pos,
"the `all(...)` override must sort BEFORE the `not(...)` default branch \
(cargo-sort compares raw cfg predicate strings byte-wise: 'a' < 'n'); got:\n{cargo_toml}"
);
assert!(
not_pos < android_pos,
"the `not(...)` default branch must sort before `target_os = \"android\"` \
('n' < 't'); got:\n{cargo_toml}"
);
toml::from_str::<toml::Value>(cargo_toml).expect("generated JNI Cargo.toml must be valid TOML");
}
#[test]
fn scaffold_jni_no_target_blocks_without_overrides() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-doc"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.example.demo"
namespace = "dev.example.demo"
features = ["full"]
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
!cargo_toml.contains("[target."),
"no [target.*] blocks without overrides; got:\n{cargo_toml}"
);
assert!(
cargo_toml.contains("demo-doc = {") && cargo_toml.contains(r#"features = ["full"]"#),
"core-crate dep must stay inline with full features; got:\n{cargo_toml}"
);
toml::from_str::<toml::Value>(cargo_toml).expect("generated JNI Cargo.toml must be valid TOML");
}
#[test]
fn scaffold_jni_section_order_matches_cargo_sort() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
let pkg = cargo_toml.find("[package]").expect("missing [package]");
let meta = cargo_toml
.find("[package.metadata.cargo-machete]")
.expect("missing [package.metadata.cargo-machete]");
let lib = cargo_toml.find("[lib]").expect("missing [lib]");
let deps = cargo_toml.find("[dependencies]").expect("missing [dependencies]");
assert!(
pkg < meta && meta < lib && lib < deps,
"section order must be [package] < [package.metadata.cargo-machete] < [lib] < [dependencies]; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_core_dep_inherits_top_level_features_when_kotlin_android_omits_them() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
features = ["native-http", "full"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
cargo_toml.contains(r#"features = ["native-http", "full"]"#),
"core dep must inherit the top-level feature set; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_core_dep_prefers_explicit_kotlin_android_features() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-llm"
sources = ["src/lib.rs"]
features = ["native-http", "full"]
[crates.kotlin_android]
package = "dev.sample_crate.demo"
namespace = "dev.sample_crate.demo"
features = ["android-http"]
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
cargo_toml.contains(r#"features = ["android-http"]"#),
"explicit kotlin_android features must win; got:\n{cargo_toml}"
);
assert!(
!cargo_toml.contains("native-http"),
"top-level features must not leak in when overridden; got:\n{cargo_toml}"
);
}
#[test]
fn scaffold_jni_never_declares_a_capsule_package_dependency() {
let config = resolved_one(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo-lang-pack"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.example.demo"
namespace = "dev.example.demo"
[crates.kotlin_android.capsule_types.Language]
host_type = "org.example.TSLanguage"
[crates.ffi.capsule_types.Language]
into_raw_type = "tree_sitter::ffi::TSLanguage"
c_return_type = "TSLanguage"
package = "tree-sitter"
package_version = "0.26"
"#,
);
let api = ApiSurface::default();
let files = scaffold_jni(&api, &config).unwrap();
let cargo_toml = &files[0].content;
assert!(
!cargo_toml.contains("tree-sitter"),
"the JNI manifest must never declare a capsule package as a dependency: the JNI \
shim transfers capsule returns via `.into_raw()` type inference alone and never \
names the crate; got:\n{cargo_toml}"
);
toml::from_str::<toml::Value>(cargo_toml).expect("generated JNI Cargo.toml must be valid TOML");
}
}