use std::env; use std::ffi::OsString; use std::fs; use std::path::Path; use std::path::PathBuf; use std::str::FromStr; use anyhow::{anyhow, Result}; use convert_case::{Case, Casing}; use serde::Deserialize; use structopt::clap::AppSettings; use structopt::StructOpt; use toml::Value; #[derive(StructOpt, Debug, PartialEq, Deserialize, Default)] #[structopt(setting(AppSettings::DeriveDisplayOrder))] pub struct RawOpts { /// Path of input Rust code #[structopt(short, long)] pub rust_input: String, /// Path of output generated Dart code #[structopt(short, long)] pub dart_output: String, /// If provided, generated Dart declaration code to this separate file #[structopt(long)] pub dart_decl_output: Option, /// Path of output generated C header #[structopt(short, long)] pub c_output: Option>, /// Crate directory for your Rust project #[structopt(long)] pub rust_crate_dir: Option, /// Path of output generated Rust code #[structopt(long)] pub rust_output: Option, /// Generated class name #[structopt(long)] pub class_name: Option, /// Line length for dart formatting #[structopt(long)] pub dart_format_line_length: Option, /// Skip automatically adding `mod bridge_generated;` to `lib.rs` #[structopt(long)] pub skip_add_mod_to_lib: bool, /// Path to the installed LLVM #[structopt(long)] pub llvm_path: Option>, /// LLVM compiler opts #[structopt(long)] pub llvm_compiler_opts: Option, /// Path to root of Dart project, otherwise inferred from --dart-output #[structopt(long)] pub dart_root: Option, /// Skip running build_runner even when codegen-capable code is detected #[structopt(long)] pub no_build_runner: bool, /// Show debug messages. #[structopt(short, long)] pub verbose: bool, } #[derive(Debug)] pub struct Opts { pub rust_input_path: String, pub dart_output_path: String, pub dart_decl_output_path: Option, pub c_output_path: Vec, pub rust_crate_dir: String, pub rust_output_path: String, pub class_name: String, pub dart_format_line_length: i32, pub skip_add_mod_to_lib: bool, pub llvm_path: Vec, pub llvm_compiler_opts: String, pub manifest_path: String, pub dart_root: Option, pub build_runner: bool, } pub fn parse(raw: RawOpts) -> Opts { let rust_input_path = canon_path(&raw.rust_input); let rust_crate_dir = canon_path(&raw.rust_crate_dir.unwrap_or_else(|| { fallback_rust_crate_dir(&rust_input_path) .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_crate_dir"))) })); let manifest_path = { let mut path = std::path::PathBuf::from_str(&rust_crate_dir).unwrap(); path.push("Cargo.toml"); path_to_string(path).unwrap() }; let rust_output_path = canon_path(&raw.rust_output.unwrap_or_else(|| { fallback_rust_output_path(&rust_input_path) .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_output"))) })); let class_name = raw.class_name.unwrap_or_else(|| { fallback_class_name(&*rust_crate_dir) .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("class_name"))) }); let c_output_path = raw .c_output .map(|outputs| { outputs .iter() .map(|output| canon_path(output)) .collect::>() }) .unwrap_or_else(|| { vec![fallback_c_output_path() .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("c_output")))] }); let dart_root = { let dart_output = &raw.dart_output; raw.dart_root .as_deref() .map(canon_path) .or_else(|| fallback_dart_root(dart_output).ok()) }; Opts { rust_input_path, dart_output_path: canon_path(&raw.dart_output), dart_decl_output_path: raw .dart_decl_output .as_ref() .map(|s| canon_path(s.as_str())), c_output_path, rust_crate_dir, rust_output_path, class_name, dart_format_line_length: raw.dart_format_line_length.unwrap_or(80), skip_add_mod_to_lib: raw.skip_add_mod_to_lib, llvm_path: raw.llvm_path.unwrap_or_else(|| { vec![ "/opt/homebrew/opt/llvm".to_owned(), // Homebrew root "/usr/local/opt/llvm".to_owned(), // Homebrew x86-64 root // Possible Linux LLVM roots "/usr/lib/llvm-9".to_owned(), "/usr/lib/llvm-10".to_owned(), "/usr/lib/llvm-11".to_owned(), "/usr/lib/llvm-12".to_owned(), "/usr/lib/llvm-13".to_owned(), "/usr/lib/llvm-14".to_owned(), "/usr/lib/".to_owned(), "/usr/lib64/".to_owned(), "C:/Program Files/llvm".to_owned(), // Default on Windows "C:/Program Files/LLVM".to_owned(), "C:/msys64/mingw64".to_owned(), // https://packages.msys2.org/package/mingw-w64-x86_64-clang ] }), llvm_compiler_opts: raw.llvm_compiler_opts.unwrap_or_else(|| "".to_string()), manifest_path, dart_root, build_runner: !raw.no_build_runner, } } fn format_fail_to_guess_error(name: &str) -> String { format!( "fail to guess {}, please specify it manually in command line arguments", name ) } fn fallback_rust_crate_dir(rust_input_path: &str) -> Result { let mut dir_curr = Path::new(rust_input_path) .parent() .ok_or_else(|| anyhow!(""))?; loop { let path_cargo_toml = dir_curr.join("Cargo.toml"); if path_cargo_toml.exists() { return Ok(dir_curr .as_os_str() .to_str() .ok_or_else(|| anyhow!(""))? .to_string()); } if let Some(next_parent) = dir_curr.parent() { dir_curr = next_parent; } else { break; } } Err(anyhow!( "look at parent directories but none contains Cargo.toml" )) } fn fallback_c_output_path() -> Result { let named_temp_file = Box::leak(Box::new(tempfile::Builder::new().suffix(".h").tempfile()?)); Ok(named_temp_file .path() .to_str() .ok_or_else(|| anyhow!(""))? .to_string()) } fn fallback_rust_output_path(rust_input_path: &str) -> Result { Ok(Path::new(rust_input_path) .parent() .ok_or_else(|| anyhow!(""))? .join("bridge_generated.rs") .to_str() .ok_or_else(|| anyhow!(""))? .to_string()) } fn fallback_dart_root(dart_output_path: &str) -> Result { let mut res = canon_pathbuf(dart_output_path); while res.pop() { if res.join("pubspec.yaml").is_file() { return res .to_str() .map(ToString::to_string) .ok_or_else(|| anyhow!("Non-utf8 path")); } } Err(anyhow!( "Root of Dart library could not be inferred from Dart output" )) } fn fallback_class_name(rust_crate_dir: &str) -> Result { let cargo_toml_path = Path::new(rust_crate_dir).join("Cargo.toml"); let cargo_toml_content = fs::read_to_string(cargo_toml_path)?; let cargo_toml_value = cargo_toml_content.parse::()?; let package_name = cargo_toml_value .get("package") .ok_or_else(|| anyhow!("no `package` in Cargo.toml"))? .get("name") .ok_or_else(|| anyhow!("no `name` in Cargo.toml"))? .as_str() .ok_or_else(|| anyhow!(""))?; Ok(package_name.to_case(Case::Pascal)) } fn canon_path(sub_path: &str) -> String { let path = canon_pathbuf(sub_path); path_to_string(path).unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)) } fn canon_pathbuf(sub_path: &str) -> PathBuf { let mut path = env::current_dir().unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)); path.push(sub_path); path } fn path_to_string(path: PathBuf) -> Result { path.into_os_string().into_string() } impl Opts { pub fn dart_api_class_name(&self) -> String { self.class_name.clone() } pub fn dart_api_impl_class_name(&self) -> String { format!("{}Impl", self.class_name) } pub fn dart_wire_class_name(&self) -> String { format!("{}Wire", self.class_name) } /// Returns None if the path terminates in "..", or not utf8. pub fn dart_output_path_name(&self) -> Option<&str> { let name = Path::new(&self.dart_output_path); let root = name.file_name()?.to_str()?; if let Some((name, _)) = root.rsplit_once('.') { Some(name) } else { Some(root) } } pub fn dart_output_freezed_path(&self) -> Option { Some( Path::new(&self.dart_output_path) .with_extension("freezed.dart") .to_str()? .to_owned(), ) } }