Skip to content

Commit

Permalink
CLI to print/diff generated sources
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bendk committed Nov 12, 2024
1 parent 1a4a949 commit b483d11
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ members = [
"fixtures/large-enum",
"fixtures/large-error",
"fixtures/enum-types",
"tools/test-bindgen",
]

resolver = "2"
Expand Down
16 changes: 16 additions & 0 deletions docs/manual/src/Hacking.md
Original file line number Diff line number Diff line change
@@ -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 <language> print`. This will print out the generated code for the current crate.
- Run `cargo run -p test-bindgen <language> save-diff`. This will save a copy of the generated code for later steps.
- Make changes to the code in `uniffi-bindgen/src/bindings/<language>`
- Run `cargo run -p test-bindgen <language> diff`. This will print out a diff of the generated code from your last `save-diff` run.

6 changes: 4 additions & 2 deletions docs/manual/src/Upgrading.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ nav:
- ./internals/rendering_foreign_bindings.md

- ./Upgrading.md
- ./Hacking.md
1 change: 1 addition & 0 deletions tools/test-bindgen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
12 changes: 12 additions & 0 deletions tools/test-bindgen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
204 changes: 204 additions & 0 deletions tools/test-bindgen/src/main.rs
Original file line number Diff line number Diff line change
@@ -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:
/// - <make some change to the swift bindings>
/// - cargo run -p test-bindings swift diff
/// - <inspect the diff for changes>
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<Self> {
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<Package> {
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::<Vec<_>>();
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(())
}

0 comments on commit b483d11

Please sign in to comment.