Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI to print/diff generated sources #2308

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(())
}