From b483d113cfe5325f0423733119d8e8d7f675a82a Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Tue, 12 Nov 2024 12:37:01 -0500 Subject: [PATCH] CLI to print/diff generated sources This allows you to save the generated sources for some fixture/example, make some changes to the foreign bindings code, then print out a diff. I've been using this pattern when writing code, let's make it easier to use. --- CHANGELOG.md | 1 + Cargo.lock | 10 ++ Cargo.toml | 1 + docs/manual/src/Hacking.md | 16 +++ docs/manual/src/Upgrading.md | 6 +- mkdocs.yml | 1 + tools/test-bindgen/.gitignore | 1 + tools/test-bindgen/Cargo.toml | 12 ++ tools/test-bindgen/src/main.rs | 204 +++++++++++++++++++++++++++++++++ 9 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 docs/manual/src/Hacking.md create mode 100644 tools/test-bindgen/.gitignore create mode 100644 tools/test-bindgen/Cargo.toml create mode 100644 tools/test-bindgen/src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a668af669..d305134004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Kotlin: Proc-macros exporting an `impl Trait for Struct` block now has a class inheritance hierarcy to reflect that. [#2297](https://github.com/mozilla/uniffi-rs/pull/2297) +- Added `test-bindgen` tool to test changes to the foreign bindings code. [All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.28.2...HEAD). diff --git a/Cargo.lock b/Cargo.lock index 3f57522489..2a5a5d2fa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,6 +1453,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-bindgen" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", +] + [[package]] name = "textwrap" version = "0.16.0" diff --git a/Cargo.toml b/Cargo.toml index 7dd59ecb42..8d8d1bc45c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ members = [ "fixtures/large-enum", "fixtures/large-error", "fixtures/enum-types", + "tools/test-bindgen", ] resolver = "2" diff --git a/docs/manual/src/Hacking.md b/docs/manual/src/Hacking.md new file mode 100644 index 0000000000..5084f15eaa --- /dev/null +++ b/docs/manual/src/Hacking.md @@ -0,0 +1,16 @@ +# Hacking on UniFFI code + +If you're interested in hacking on UniFFI code, please do! +We're always open to outside contributions. +This page contains some tips for doing this. + +## Testing bindings code with `test-bindgen` + +Use the `test-bindgen` tool to test out changes to the foreign bindings generation code. + +- `cd` to a fixture/example directory +- Run `cargo run -p test-bindgen print`. This will print out the generated code for the current crate. +- Run `cargo run -p test-bindgen save-diff`. This will save a copy of the generated code for later steps. +- Make changes to the code in `uniffi-bindgen/src/bindings/` +- Run `cargo run -p test-bindgen diff`. This will print out a diff of the generated code from your last `save-diff` run. + diff --git a/docs/manual/src/Upgrading.md b/docs/manual/src/Upgrading.md index c685a43cf2..ca43ec82b3 100644 --- a/docs/manual/src/Upgrading.md +++ b/docs/manual/src/Upgrading.md @@ -1,6 +1,8 @@ -# v0.28.x -> v0.29.x +# Upgrade guide -## Custom types +## v0.28.x -> v0.29.x + +### Custom types Custom types are now implemented using a macro rather than implementing the `UniffiCustomTypeConverter` trait, addressing some edge-cases with custom types wrapping types from other crates (eg, Url). diff --git a/mkdocs.yml b/mkdocs.yml index beaaa31f94..3ce4df5244 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,3 +84,4 @@ nav: - ./internals/rendering_foreign_bindings.md - ./Upgrading.md +- ./Hacking.md diff --git a/tools/test-bindgen/.gitignore b/tools/test-bindgen/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/tools/test-bindgen/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tools/test-bindgen/Cargo.toml b/tools/test-bindgen/Cargo.toml new file mode 100644 index 0000000000..897b86f72f --- /dev/null +++ b/tools/test-bindgen/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-bindgen" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +camino = "1.0.8" +cargo_metadata = "0.15" +clap = { version = "4", features = ["cargo", "std", "derive"] } diff --git a/tools/test-bindgen/src/main.rs b/tools/test-bindgen/src/main.rs new file mode 100644 index 0000000000..d3167890bf --- /dev/null +++ b/tools/test-bindgen/src/main.rs @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::{ + env::consts::DLL_EXTENSION, + fs, + process::{Command, Stdio}, +}; + +use anyhow::{bail, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use cargo_metadata::{Message, Metadata, MetadataCommand, Package}; +use clap::{Parser, Subcommand}; + +fn main() { + run_main().unwrap(); +} + +fn run_main() -> Result<()> { + let args = Cli::parse(); + let context = Context::new("swift")?; + match args.command { + Commands::Print => { + let out_dir = context.out_dir_base.join("print"); + generate_sources(&context, args.language, &out_dir)?; + print_sources(&out_dir).unwrap(); + } + Commands::SaveDiff => { + let out_dir = context.out_dir_base.join("old"); + generate_sources(&context, args.language, &out_dir)?; + } + Commands::Diff => { + let out_dir = context.out_dir_base.join("new"); + generate_sources(&context, args.language, &out_dir)?; + diff_sources(&context.out_dir_base)?; + } + }; + Ok(()) +} + +/// Scaffolding and bindings generator for Rust +#[derive(Parser)] +#[clap(name = "uniffi-bindgen")] +#[clap(version = clap::crate_version!())] +#[clap(propagate_version = true)] +struct Cli { + language: TargetLanguage, + #[clap(subcommand)] + command: Commands, +} + +/// Enumeration of all foreign language targets currently supported by our CLI. +/// +#[derive(Copy, Clone, Eq, PartialEq, Hash, clap::ValueEnum)] +enum TargetLanguage { + Kotlin, + Swift, + Python, + Ruby, +} + +#[derive(Subcommand)] +enum Commands { + /// Print out the generated source + Print, + /// Save the generated source to a target directory for future `diff` commands + SaveDiff, + /// Run a diff of the generated sources against the last `save-diff` command + /// + /// Usage: + /// + /// - cargo run -p test-bindings swift save-diff + /// - Loop: + /// - + /// - cargo run -p test-bindings swift diff + /// - + Diff, +} + +#[derive(Debug)] +struct Context { + // Name of the crate we're generating source for + crate_name: String, + // Path to the cdylib for the crate + cdylib_path: Utf8PathBuf, + // Base directory for writing generated files to + out_dir_base: Utf8PathBuf, +} + +impl Context { + fn new(language: &str) -> Result { + let metadata = MetadataCommand::new().exec()?; + let package = Self::find_current_package(&metadata)?; + let (crate_name, cdylib_path) = Self::find_crate_and_cdylib(&package)?; + let out_dir_base = metadata + .target_directory + .join("test-bindgen") + .join(language) + .join(&crate_name); + + Ok(Self { + out_dir_base, + cdylib_path, + crate_name, + }) + } + + fn find_current_package(metadata: &Metadata) -> Result { + let current_dir = Utf8PathBuf::try_from(std::env::current_dir()?)?; + for package in &metadata.packages { + if current_dir.starts_with(package.manifest_path.parent().unwrap()) { + return Ok(package.clone()); + } + } + bail!("Can't determine current package (current_dir: {current_dir})") + } + + fn find_crate_and_cdylib(package: &Package) -> Result<(String, Utf8PathBuf)> { + let cdylib_targets = package + .targets + .iter() + .filter(|t| t.crate_types.iter().any(|t| t == "cdylib")) + .collect::>(); + let target = match cdylib_targets.len() { + 1 => cdylib_targets[0], + n => bail!("Found {n} cdylib targets for {}", package.name), + }; + + let mut command = Command::new("cargo") + .args(["build", "--message-format=json-render-diagnostics"]) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let reader = std::io::BufReader::new(command.stdout.take().unwrap()); + for message in cargo_metadata::Message::parse_stream(reader) { + if let Message::CompilerArtifact(artifact) = message? { + if artifact.target == *target { + for filename in artifact.filenames.iter() { + if matches!(filename.extension(), Some(DLL_EXTENSION)) { + return Ok((target.name.clone(), filename.clone())); + } + } + } + } + } + bail!("cdylib not found for crate {}", package.name) + } +} + +fn generate_sources(context: &Context, language: TargetLanguage, dir: &Utf8Path) -> Result<()> { + let language = match language { + TargetLanguage::Swift => "swift", + TargetLanguage::Kotlin => "kotlin", + TargetLanguage::Python => "python", + TargetLanguage::Ruby => "ruby", + }; + let code = Command::new("cargo") + .args([ + "run", + "-p", + "uniffi-bindgen-cli", + "generate", + "--language", + language, + "--out-dir", + dir.as_str(), + "--library", + context.cdylib_path.as_str(), + "--crate", + &context.crate_name, + ]) + .spawn()? + .wait()? + .code(); + match code { + Some(0) => Ok(()), + Some(code) => bail!("uniffi-bindgen-cli exited with {code}"), + None => bail!("uniffi-bindgen-cli terminated by signal"), + } +} + +fn print_sources(dir: &Utf8Path) -> Result<()> { + for entry in fs::read_dir(dir)? { + let path = entry?.path(); + let contents = fs::read_to_string(&path)?; + println!( + "-------------------- {} --------------------", + path.file_name().unwrap().to_string_lossy() + ); + println!("{contents}"); + println!(); + } + Ok(()) +} + +fn diff_sources(out_dir_base: &Utf8Path) -> Result<()> { + Command::new("diff") + .args(["-dur", "old", "new", "--color=auto"]) + .current_dir(out_dir_base) + .spawn()? + .wait()?; + Ok(()) +}