use anyhow::{Context, Result, bail};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{CrateType, MetadataCommand, Package, Target};
use std::io::{Read, Write};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{env, fs};
use uniffi_bindgen::{BindgenLoader, BindgenPaths};
use uniffi_bindgen_java::{GenerateOptions, generate};
use uniffi_testing::UniFFITestHelper;
fn run_test(fixture_name: &str, test_file: &str) -> Result<()> {
let test_path = Utf8Path::new(".").join("tests").join(test_file);
let test_helper = UniFFITestHelper::new(fixture_name)?;
let out_dir = test_helper.create_out_dir(env!("CARGO_TARGET_TMPDIR"), &test_path)?;
let cdylib_path = test_helper.cdylib_path()?;
let maybe_new_uniffi_toml_filename = {
let maybe_base_uniffi_toml_string =
find_uniffi_toml(fixture_name)?.and_then(read_file_contents);
let maybe_extra_uniffi_toml_string =
read_file_contents(test_path.with_file_name("uniffi-extras.toml"));
let final_string: String = itertools::Itertools::intersperse(
vec![
maybe_base_uniffi_toml_string,
maybe_extra_uniffi_toml_string,
]
.into_iter()
.flatten(),
"\n".to_string(),
)
.collect();
if final_string.is_empty() {
None
} else {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_nanos();
let new_filename =
out_dir.with_file_name(format!("{}-{}.toml", fixture_name, current_time));
write_file_contents(&new_filename, &final_string)?;
Some(new_filename)
}
};
let mut paths = BindgenPaths::default();
if let Some(config_path) = &maybe_new_uniffi_toml_filename {
paths.add_config_override_layer(config_path.clone());
}
paths.add_cargo_metadata_layer(false)?;
let loader = BindgenLoader::new(paths);
generate(
&loader,
&GenerateOptions {
source: cdylib_path.clone(),
out_dir: out_dir.clone(),
format: true,
crate_filter: None,
},
)?;
let native_lib_dir = out_dir.join("native");
fs::create_dir_all(&native_lib_dir)?;
let cdylib_filename = cdylib_path.file_name().unwrap();
let cdylib_dest = native_lib_dir.join(cdylib_filename);
fs::copy(&cdylib_path, &cdylib_dest)?;
let extension = cdylib_path.extension().unwrap(); let lib_base_name = cdylib_filename
.strip_prefix("lib")
.unwrap_or(cdylib_filename)
.split('-')
.next()
.unwrap_or(cdylib_filename);
let expected_lib_name = format!("lib{}.{}", lib_base_name, extension);
let symlink_path = native_lib_dir.join(&expected_lib_name);
if !symlink_path.exists() {
std::os::unix::fs::symlink(cdylib_dest.file_name().unwrap(), &symlink_path)?;
}
let jar_file = build_jar(fixture_name, &out_dir)?;
let status = Command::new("javac")
.arg("-classpath")
.arg(calc_classpath(vec![&out_dir, &jar_file]))
.arg("-Werror")
.arg(&test_path)
.spawn()
.context("Failed to spawn `javac` to compile Java test")?
.wait()
.context("Failed to wait for `javac` when compiling Java test")?;
if !status.success() {
anyhow::bail!("running `javac` failed when compiling the Java test")
}
let compiled_path = test_path.file_stem().unwrap();
let run_status = Command::new("java")
.arg("-ea")
.arg("--enable-native-access=ALL-UNNAMED")
.arg(format!("-Djava.library.path={}", native_lib_dir))
.arg("-classpath")
.arg(calc_classpath(vec![
&out_dir,
&jar_file,
&test_path.parent().unwrap().to_path_buf(),
]))
.arg(compiled_path)
.spawn()
.context("Failed to spawn `java` to run Java test")?
.wait()
.context("Failed to wait for `java` when running Java test")?;
if !run_status.success() {
anyhow::bail!("Running the `java` test failed.")
}
Ok(())
}
fn find_uniffi_toml(name: &str) -> Result<Option<Utf8PathBuf>> {
let metadata = MetadataCommand::new()
.exec()
.expect("error running cargo metadata");
let matching: Vec<&Package> = metadata
.packages
.iter()
.filter(|p| p.name == name)
.collect();
let package = match matching.len() {
1 => matching[0].clone(),
n => bail!("cargo metadata return {n} packages named {name}"),
};
let cdylib_targets: Vec<&Target> = package
.targets
.iter()
.filter(|t| t.crate_types.iter().any(|t| t == &CrateType::CDyLib))
.collect();
let target = match cdylib_targets.len() {
1 => cdylib_targets[0],
n => bail!("Found {n} cdylib targets for {}", package.name),
};
let maybe_uniffi_toml = target
.src_path
.parent()
.map(|uniffi_toml_dir| uniffi_toml_dir.with_file_name("uniffi.toml"));
Ok(maybe_uniffi_toml)
}
fn build_jar(fixture_name: &str, out_dir: &Utf8PathBuf) -> Result<Utf8PathBuf> {
let mut jar_file = Utf8PathBuf::from(out_dir);
jar_file.push(format!("{}.jar", fixture_name));
let staging_dir = out_dir.join("staging");
let status = Command::new("javac")
.arg("-Werror")
.arg("-d")
.arg(&staging_dir)
.arg("-classpath")
.arg(calc_classpath(vec![]))
.args(
glob::glob(&out_dir.join("**/*.java").into_string())?
.flatten()
.map(|p| String::from(p.to_string_lossy())),
)
.spawn()
.context("Failed to spawn `javac` to compile the bindings")?
.wait()
.context("Failed to wait for `javac` when compiling the bindings")?;
if !status.success() {
bail!("running `javac` failed when compiling the bindings")
}
let jar_status = Command::new("jar")
.current_dir(out_dir)
.arg("cf")
.arg(jar_file.file_name().unwrap())
.arg("-C")
.arg(&staging_dir)
.arg(".")
.spawn()
.context("Failed to spawn `jar` to package the bindings")?
.wait()
.context("Failed to wait for `jar` when packaging the bindings")?;
if !jar_status.success() {
bail!("running `jar` failed")
}
Ok(jar_file)
}
fn calc_classpath(extra_paths: Vec<&Utf8PathBuf>) -> String {
extra_paths
.into_iter()
.map(|p| p.to_string())
.chain(env::var("CLASSPATH"))
.collect::<Vec<String>>()
.join(":")
}
fn read_file_contents(path: Utf8PathBuf) -> Option<String> {
if let Ok(metadata) = fs::metadata(&path) {
if metadata.is_file() {
let mut content = String::new();
std::fs::File::open(path)
.ok()?
.read_to_string(&mut content)
.ok()?;
Some(content)
} else {
None
}
} else {
None
}
}
fn write_file_contents(path: &Utf8PathBuf, contents: &str) -> Result<()> {
std::fs::File::create(path)?.write_all(contents.as_bytes())?;
Ok(())
}
macro_rules! fixture_tests {
{
$(($test_name:ident, $fixture_name:expr, $test_script:expr),)*
} => {
$(
#[test]
fn $test_name() -> Result<()> {
run_test($fixture_name, $test_script)
}
)*
}
}
fixture_tests! {
(test_arithmetic, "uniffi-example-arithmetic", "scripts/TestArithmetic.java"),
(test_geometry, "uniffi-example-geometry", "scripts/TestGeometry.java"),
(test_rondpoint, "uniffi-example-rondpoint", "scripts/TestRondpoint.java"),
(test_sprites, "uniffi-example-sprites", "scripts/TestSprites.java"),
(test_coverall, "uniffi-fixture-coverall", "scripts/TestFixtureCoverall.java"),
(test_chronological, "uniffi-fixture-time", "scripts/TestChronological.java"),
(test_custom_types, "uniffi-example-custom-types", "scripts/TestCustomTypes/TestCustomTypes.java"),
(test_external_types, "uniffi-fixture-ext-types", "scripts/TestImportedTypes/TestImportedTypes.java"),
(test_futures, "uniffi-example-futures", "scripts/TestFutures.java"),
(test_futures_fixtures, "uniffi-fixture-futures", "scripts/TestFixtureFutures/TestFixtureFutures.java"),
(test_trait_methods, "uniffi-fixture-trait-methods", "scripts/TestTraitMethods.java"),
(test_omit_checksums, "uniffi-example-arithmetic", "scripts/TestOmitChecksums/TestOmitChecksums.java"),
(test_proc_macro, "uniffi-fixture-proc-macro", "scripts/TestProcMacro.java"),
(test_rename, "uniffi-fixture-rename", "scripts/TestRename/TestRename.java"),
(test_primitive_arrays, "uniffi-fixture-primitive-arrays", "scripts/TestPrimitiveArrays.java"),
}