#![warn(rust_2018_idioms)]
#![allow(unknown_lints)]
const BINDGEN_VERSION: &str = env!("CARGO_PKG_VERSION");
use anyhow::{anyhow, bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::io::prelude::*;
use std::{
collections::HashMap,
env,
fs::File,
path::{Path, PathBuf},
process::Command,
};
pub mod bindings;
pub mod interface;
pub mod scaffolding;
use bindings::TargetLanguage;
use interface::ComponentInterface;
use scaffolding::RustScaffolding;
pub fn generate_component_scaffolding<P: AsRef<Path>>(
udl_file: P,
config_file_override: Option<P>,
out_dir_override: Option<P>,
manifest_path_override: Option<P>,
format_code: bool,
) -> Result<()> {
let manifest_path_override = manifest_path_override.as_ref().map(|p| p.as_ref());
let config_file_override = config_file_override.as_ref().map(|p| p.as_ref());
let out_dir_override = out_dir_override.as_ref().map(|p| p.as_ref());
let udl_file = udl_file.as_ref();
let component = parse_udl(&udl_file)?;
let _config = get_config(&component, udl_file, config_file_override);
ensure_versions_compatibility(&udl_file, manifest_path_override)?;
let mut filename = Path::new(&udl_file)
.file_stem()
.ok_or_else(|| anyhow!("not a file"))?
.to_os_string();
filename.push(".uniffi.rs");
let mut out_dir = get_out_dir(&udl_file, out_dir_override)?;
out_dir.push(filename);
let mut f =
File::create(&out_dir).map_err(|e| anyhow!("Failed to create output file: {:?}", e))?;
write!(f, "{}", RustScaffolding::new(&component))
.map_err(|e| anyhow!("Failed to write output file: {:?}", e))?;
if format_code {
Command::new("rustfmt").arg(&out_dir).status()?;
}
Ok(())
}
fn ensure_versions_compatibility(
udl_file: &Path,
manifest_path_override: Option<&Path>,
) -> Result<()> {
let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
match manifest_path_override {
Some(p) => {
metadata_cmd.manifest_path(p);
}
None => {
metadata_cmd.current_dir(guess_crate_root(udl_file)?);
}
};
let metadata = metadata_cmd
.exec()
.map_err(|e| anyhow!("Failed to run cargo metadata: {:?}", e))?;
let uniffi_runtime_deps: Vec<cargo_metadata::Package> = metadata
.packages
.into_iter()
.filter(|p| p.name == "uniffi")
.collect();
if uniffi_runtime_deps.is_empty() {
bail!("It looks like the crate doesn't depend on the `uniffi` runtime. Please add `uniffi` as a dependency.");
}
if uniffi_runtime_deps.len() > 1 {
bail!("It looks like the workspace depends on multiple versions of `uniffi`. Please rectify the problem and try again.");
}
let uniffi_runtime_version = uniffi_runtime_deps[0].version.to_string();
if uniffi_runtime_version != BINDGEN_VERSION {
bail!("The `uniffi` dependency version ({}) is different than `uniffi-bindgen` own version ({}). Please rectify the problem and try again.", uniffi_runtime_version, BINDGEN_VERSION);
}
Ok(())
}
pub fn generate_bindings<P: AsRef<Path>>(
udl_file: P,
config_file_override: Option<P>,
target_languages: Vec<&str>,
out_dir_override: Option<P>,
try_format_code: bool,
) -> Result<()> {
let out_dir_override = out_dir_override.as_ref().map(|p| p.as_ref());
let config_file_override = config_file_override.as_ref().map(|p| p.as_ref());
let udl_file = udl_file.as_ref();
let component = parse_udl(&udl_file)?;
let config = get_config(&component, udl_file, config_file_override)?;
let out_dir = get_out_dir(&udl_file, out_dir_override)?;
for language in target_languages {
bindings::write_bindings(
&config.bindings,
&component,
&out_dir,
language.try_into()?,
try_format_code,
false,
)?;
}
Ok(())
}
pub fn run_tests<P: AsRef<Path>>(
cdylib_dir: P,
udl_file: P,
test_scripts: Vec<&str>,
config_file_override: Option<P>,
) -> Result<()> {
let cdylib_dir = cdylib_dir.as_ref();
let udl_file = udl_file.as_ref();
let config_file_override = config_file_override.as_ref().map(|p| p.as_ref());
let component = parse_udl(&udl_file)?;
let config = get_config(&component, udl_file, config_file_override)?;
let mut language_tests: HashMap<TargetLanguage, Vec<String>> = HashMap::new();
for test_script in test_scripts {
let lang: TargetLanguage = PathBuf::from(test_script)
.extension()
.ok_or_else(|| anyhow!("File has no extension!"))?
.try_into()?;
language_tests
.entry(lang)
.or_default()
.push(test_script.to_owned());
}
for (lang, test_scripts) in language_tests {
bindings::write_bindings(&config.bindings, &component, &cdylib_dir, lang, true, true)?;
bindings::compile_bindings(&config.bindings, &component, &cdylib_dir, lang)?;
for test_script in test_scripts {
bindings::run_script(cdylib_dir, &test_script, lang)?;
}
}
Ok(())
}
fn guess_crate_root(udl_file: &Path) -> Result<&Path> {
let path_guess = udl_file
.parent()
.ok_or_else(|| anyhow!("UDL file has no parent folder!"))?
.parent()
.ok_or_else(|| anyhow!("UDL file has no grand-parent folder!"))?;
if !path_guess.join("Cargo.toml").is_file() {
bail!("UDL file does not appear to be inside a crate")
}
Ok(path_guess)
}
fn get_config(
component: &ComponentInterface,
udl_file: &Path,
config_file_override: Option<&Path>,
) -> Result<Config> {
let default_config: Config = component.into();
let config_file: Option<PathBuf> = match config_file_override {
Some(cfg) => Some(PathBuf::from(cfg)),
None => {
let crate_root = guess_crate_root(udl_file)?.join("uniffi.toml");
match crate_root.canonicalize() {
Ok(f) => Some(f),
Err(_) => None,
}
}
};
match config_file {
Some(path) => {
let contents = slurp_file(&path)
.with_context(|| format!("Failed to read config file from {:?}", &path))?;
let loaded_config: Config = toml::de::from_str(&contents)
.with_context(|| format!("Failed to generate config from file {:?}", &path))?;
Ok(loaded_config.merge_with(&default_config))
}
None => Ok(default_config),
}
}
fn get_out_dir(udl_file: &Path, out_dir_override: Option<&Path>) -> Result<PathBuf> {
Ok(match out_dir_override {
Some(s) => {
std::fs::create_dir_all(&s)?;
s.canonicalize()
.map_err(|e| anyhow!("Unable to find out-dir: {:?}", e))?
}
None => udl_file
.parent()
.ok_or_else(|| anyhow!("File has no parent directory"))?
.to_owned(),
})
}
fn parse_udl(udl_file: &Path) -> Result<ComponentInterface> {
let udl =
slurp_file(udl_file).map_err(|_| anyhow!("Failed to read UDL from {:?}", &udl_file))?;
udl.parse::<interface::ComponentInterface>()
.map_err(|e| anyhow!("Failed to parse UDL: {}", e))
}
fn slurp_file(file_name: &Path) -> Result<String> {
let mut contents = String::new();
let mut f = File::open(file_name)?;
f.read_to_string(&mut contents)?;
Ok(contents)
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Config {
#[serde(default)]
bindings: bindings::Config,
}
impl From<&ComponentInterface> for Config {
fn from(ci: &ComponentInterface) -> Self {
Config {
bindings: ci.into(),
}
}
}
pub trait MergeWith {
fn merge_with(&self, other: &Self) -> Self;
}
impl MergeWith for Config {
fn merge_with(&self, other: &Self) -> Self {
Config {
bindings: self.bindings.merge_with(&other.bindings),
}
}
}
impl<T: Clone> MergeWith for Option<T> {
fn merge_with(&self, other: &Self) -> Self {
match (self, other) {
(Some(_), _) => self.clone(),
(None, Some(_)) => other.clone(),
(None, None) => None,
}
}
}
pub fn run_main() -> Result<()> {
const POSSIBLE_LANGUAGES: &[&str] = &["kotlin", "python", "swift", "gecko_js"];
let matches = clap::App::new("uniffi-bindgen")
.about("Scaffolding and bindings generator for Rust")
.version(clap::crate_version!())
.subcommand(
clap::SubCommand::with_name("generate")
.about("Generate foreign language bindings")
.arg(
clap::Arg::with_name("language")
.required(true)
.takes_value(true)
.long("--language")
.short("-l")
.multiple(true)
.number_of_values(1)
.possible_values(&POSSIBLE_LANGUAGES)
.help("Foreign language(s) for which to build bindings"),
)
.arg(
clap::Arg::with_name("out_dir")
.long("--out-dir")
.short("-o")
.takes_value(true)
.help("Directory in which to write generated files. Default is same folder as .udl file."),
)
.arg(
clap::Arg::with_name("no_format")
.long("--no-format")
.help("Do not try to format the generated bindings"),
)
.arg(clap::Arg::with_name("udl_file").required(true))
.arg(
clap::Arg::with_name("config")
.long("--config-path")
.takes_value(true)
.help("Path to the optional uniffi config file. If not provided, uniffi-bindgen will try to guess it from the UDL's file location.")
),
)
.subcommand(
clap::SubCommand::with_name("scaffolding")
.about("Generate Rust scaffolding code")
.arg(
clap::Arg::with_name("out_dir")
.long("--out-dir")
.short("-o")
.takes_value(true)
.help("Directory in which to write generated files. Default is same folder as .udl file."),
)
.arg(
clap::Arg::with_name("manifest")
.long("--manifest-path")
.takes_value(true)
.help("Path to crate's Cargo.toml. If not provided, Cargo.toml is assumed to be in the UDL's file parent folder.")
)
.arg(
clap::Arg::with_name("config")
.long("--config-path")
.takes_value(true)
.help("Path to the optional uniffi config file. If not provided, uniffi-bindgen will try to guess it from the UDL's file location.")
)
.arg(
clap::Arg::with_name("no_format")
.long("--no-format")
.help("Do not format the generated code with rustfmt (useful for maintainers)"),
)
.arg(clap::Arg::with_name("udl_file").required(true)),
)
.subcommand(
clap::SubCommand::with_name("test")
.about("Run test scripts against foreign language bindings")
.arg(clap::Arg::with_name("cdylib_dir").required(true).help("Path to the directory containing the cdylib the scripts will be testing against."))
.arg(clap::Arg::with_name("udl_file").required(true))
.arg(clap::Arg::with_name("test_scripts").required(true).multiple(true).help("Foreign language(s) test scripts to run"))
.arg(
clap::Arg::with_name("config")
.long("--config-path")
.takes_value(true)
.help("Path to the optional uniffi config file. If not provided, uniffi-bindgen will try to guess from the UDL's file location.")
)
)
.get_matches();
match matches.subcommand() {
("generate", Some(m)) => crate::generate_bindings(
m.value_of_os("udl_file").unwrap(),
m.value_of_os("config"),
m.values_of("language").unwrap().collect(),
m.value_of_os("out_dir"),
!m.is_present("no_format"),
)?,
("scaffolding", Some(m)) => crate::generate_component_scaffolding(
m.value_of_os("udl_file").unwrap(),
m.value_of_os("config"),
m.value_of_os("out_dir"),
m.value_of_os("manifest"),
!m.is_present("no_format"),
)?,
("test", Some(m)) => crate::run_tests(
m.value_of_os("cdylib_dir").unwrap(),
m.value_of_os("udl_file").unwrap(),
m.values_of("test_scripts").unwrap().collect(),
m.value_of_os("config"),
)?,
_ => bail!("No command specified; try `--help` for some help."),
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_guessing_of_crate_root_directory_from_udl_file() {
let this_crate_root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let example_crate_root = this_crate_root
.parent()
.expect("should have a parent directory")
.join("./examples/arithmetic");
assert_eq!(
guess_crate_root(&example_crate_root.join("./src/arthmetic.udl")).unwrap(),
example_crate_root
);
let not_a_crate_root = &this_crate_root.join("./src/templates");
assert!(guess_crate_root(¬_a_crate_root.join("./src/example.udl")).is_err());
}
}