Skip to content

Commit

Permalink
introduce a build-rs and a cargo-duchess utility (#187)
Browse files Browse the repository at this point in the history
* introduce ClassDecl wrapper struct

* introduce build-rs lib, add to integration tests

Doesn't really do anything yet, but it does
parse and validate the various duchess macros.

* create a `cargo duchess` utility

* start writing stuff for book

* introduce JavapClassInfo for stuff from javap

...and a trait that lets you be generic over
JavapClassInfo or ClassInfo.

Distinguishing `JavapClassInfo` means we know
when the data comes from the "source of truth"
vs the user, but it also means we can serialize
because there is no span.

* Fix classpath concatenation on Windows

---------

Co-authored-by: Niko Matsakis <[email protected]>
Co-authored-by: Russell Cohen <[email protected]>
  • Loading branch information
3 people authored Oct 23, 2024
1 parent 0b2975a commit eba2cee
Show file tree
Hide file tree
Showing 32 changed files with 1,252 additions and 91 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["duchess-reflect", "macro-rules"]
members = [ "cargo-duchess","duchess-build-rs", "duchess-reflect", "macro-rules"]

[workspace.package]
version = "0.3.0"
Expand Down
4 changes: 2 additions & 2 deletions book/src/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ logger
Check out the...

* [Rustdoc](./rustdoc/doc/duchess/index.html)
* The [examples](https://github.com/duchess-rs/duchess/tree/main/test-crates/duchess-java-tests/tests/ui/examples)
* The [tutorials](https://duchess-rs.github.io/duchess/tutorials.html) chapter
* The [examples](https://github.com/duchess-rs/duchess/tree/main/test-crates/duchess-java-tests/tests/ui/examples) directory
* The [tutorials](./tutorials.md) chapter

## Curious to get involved?

Expand Down
20 changes: 19 additions & 1 deletion book/src/setup.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Setup instructions

## JDK and JAVA_HOME
## TL;DR

You need to...

* [Install the JDK](#jdk-and-java_home)
* Install the `cargo-duchess` CLI tool with `cargo install cargo-duchess`
* Run `cargo duchess init` in your package, which will add duches to your `build.rs` file and your `Cargo.toml`

## Prequisites

### JDK and JAVA_HOME

You'll need to have a modern JDK installed. We recommend JDK17 or higher. Any JDK distribution will work. Here are some recommended options:

Expand All @@ -16,6 +26,14 @@ You'll need to have a modern JDK installed. We recommend JDK17 or higher. Any JD

Duchess relies on `javap` to reflect Java type information at build time. It will *not* be invoked at runtime.

## Basic setup

To use Duchess your project requires a `build.rs` as well as a proc-macro crate. The `build.rs` does the heavy lifting, invoking javap and doing other reflection. The proc-macro crates then do final processing to generate the code.

You can

## Other details

## Configuring the CLASSPATH

You will likely want to configure the `CLASSPATH` for your Rust project as well. Like with `JAVA_HOME`, you can do that via Cargo by creating a `.cargo/config.toml` file.
Expand Down
12 changes: 12 additions & 0 deletions cargo-duchess/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "cargo-duchess"
edition = "2021"
version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
anyhow = "1.0.89"
structopt = "0.3.26"
72 changes: 72 additions & 0 deletions cargo-duchess/src/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::{path::PathBuf, process::Command};

use structopt::StructOpt;

#[derive(StructOpt, Debug)]
pub struct InitOptions {
/// Directory of the project
#[structopt(short, long, default_value = ".")]
dir: PathBuf,
}

const DEFAULT_BUILD_RS: &str = "
// This file is automatically generated by `cargo duchess init`.
use duchess_build_rs::DuchessBuildRs;
fn main() -> anyhow::Result<()> {
DuchessBuildRs::new().execute()
}
";

const ONE_LINE: &str = "duchess_build_rs::DuchessBuildRs::new().execute().unwrap()";

pub fn init(options: InitOptions) -> anyhow::Result<()> {
let InitOptions { dir } = options;

if !dir.exists() {
anyhow::bail!("directory `{}` not found", dir.display());
}

if !dir.is_dir() {
anyhow::bail!("`{}` is not a directory", dir.display());
}

let cargo_toml_path = dir.join("Cargo.toml");
if !cargo_toml_path.exists() {
anyhow::bail!(
"directory `{}` does not contain a `Cargo.toml`",
dir.display()
);
}

Command::new("cargo")
.arg("add")
.arg("duchess")
.current_dir(&dir)
.spawn()?
.wait()?;

Command::new("cargo")
.arg("add")
.arg("--build")
.arg("duchess-build-rs")
.current_dir(&dir)
.spawn()?
.wait()?;

// If `build.rs` does not exist, initialize it with default.
let build_rs_path = dir.join("build.rs");
if !build_rs_path.exists() {
std::fs::write(&build_rs_path, DEFAULT_BUILD_RS)?;
} else {
// Tell user to add it themselves
// (FIXME: use syn to insert it)
eprintln!("Warning: `build.rs` already exists. Please add the following line to `main`:");
eprintln!();
eprintln!("```rust");
eprintln!("{}", ONE_LINE);
eprintln!("```");
}

Ok(())
}
30 changes: 30 additions & 0 deletions cargo-duchess/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "cargo-duchess")]
enum Opt {
/// Initialize something
Init {
#[structopt(flatten)]
options: init::InitOptions,
},
/// Package something
Package {
/// Path to the package
#[structopt(short, long)]
path: String,
},
}

mod init;

fn main() -> anyhow::Result<()> {
let opt = Opt::from_args();
match opt {
Opt::Init { options } => {
init::init(options)?;
}
Opt::Package { path: _ } => todo!(),
}
Ok(())
}
19 changes: 19 additions & 0 deletions duchess-build-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "duchess-build-rs"
edition = "2021"
version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true

[dependencies]
anyhow = "1.0.86"
duchess-reflect = { version = "0.3.0", path = "../duchess-reflect" }
lazy_static = "1.5.0"
proc-macro2 = "1.0.86"
quote = "1.0.36"
regex = "1.10.5"
syn = { version = "2.0.71", features = ["full"] }
tempfile = "3.10.1"
walkdir = "2.5.0"
36 changes: 36 additions & 0 deletions duchess-build-rs/src/code_writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use core::fmt;
use std::io::Write;

pub struct CodeWriter<'w> {
writer: &'w mut dyn Write,
indent: usize,
}

impl<'w> CodeWriter<'w> {
pub fn new(writer: &'w mut dyn Write) -> Self {
CodeWriter { writer, indent: 0 }
}

pub fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> anyhow::Result<()> {
let mut string = String::new();
fmt::write(&mut string, fmt).unwrap();

if string.starts_with("}") || string.starts_with(")") || string.starts_with("]") {
self.indent -= 1;
}

write!(
self.writer,
"{:indent$}{}\n",
"",
string,
indent = self.indent * 4
)?;

if string.ends_with("{") || string.ends_with("(") || string.ends_with("[") {
self.indent += 1;
}

Ok(())
}
}
74 changes: 74 additions & 0 deletions duchess-build-rs/src/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::path::{Path, PathBuf};

use walkdir::WalkDir;

pub(crate) struct File {
pub(crate) path: PathBuf,
pub(crate) contents: String,
}

pub fn rs_files(path: &Path) -> impl Iterator<Item = anyhow::Result<File>> {
WalkDir::new(path)
.into_iter()
.filter_map(|entry| -> Option<anyhow::Result<File>> {
match entry {
Ok(entry) => {
if entry.path().extension().map_or(false, |e| e == "rs") {
Some(Ok(File {
path: entry.path().to_path_buf(),
contents: match std::fs::read_to_string(entry.path()) {
Ok(s) => s,
Err(err) => return Some(Err(err.into())),
},
}))
} else {
None
}
}

Err(err) => Some(Err(err.into())),
}
})
}

impl File {
/// Return a string that can be used as a slug for error messages.
pub fn slug(&self, offset: usize) -> String {
let line_num = self.contents[..offset].lines().count();
let column_num = 1 + self.contents[..offset]
.rfind('\n')
.map_or(offset, |i| offset - i - 1);
format!(
"{path}:{line_num}:{column_num}:",
path = self.path.display(),
)
}

/// Returns a chunk of rust code starting at `offset`
/// and extending until the end of the current token tree
/// or file, whichever comes first.
///
/// This is used when we are preprocessing and we find
/// some kind of macro invocation. We want to grab all
/// the text that may be part of it and pass it into `syn`.
pub fn rust_slice_from(&self, offset: usize) -> &str {
let mut counter = 0;
let terminator = self.contents[offset..].char_indices().find(|&(_, c)| {
if c == '{' || c == '[' || c == '(' {
counter += 1;
} else if c == '}' || c == ']' || c == ')' {
if counter == 0 {
return true;
}

counter -= 1;
}

false
});
match terminator {
Some((i, _)) => &self.contents[offset..offset + i],
None => &self.contents[offset..],
}
}
}
60 changes: 60 additions & 0 deletions duchess-build-rs/src/impl_java_trait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use duchess_reflect::{class_info::ClassRef, reflect::Reflector};
use proc_macro2::{Span, TokenStream};
use syn::spanned::Spanned;

use crate::{files::File, java_compiler::JavaCompiler, shim_writer::ShimWriter};

pub fn process_impl(compiler: &JavaCompiler, file: &File, offset: usize) -> anyhow::Result<()> {
let the_impl: JavaInterfaceImpl = syn::parse_str(file.rust_slice_from(offset))?;
the_impl.generate_shim(compiler)?;
Ok(())
}

struct JavaInterfaceImpl {
item: syn::ItemImpl,
}

impl syn::parse::Parse for JavaInterfaceImpl {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
// we are parsing an input that starts with an impl and then has add'l stuff
let item: syn::ItemImpl = input.parse()?;

// syn reports an error if there is anything unconsumed, so consume all remaining tokens
// after we parse the impl
let _more_tokens: TokenStream = input.parse()?;

Ok(Self { item })
}
}

impl JavaInterfaceImpl {
fn generate_shim(&self, compiler: &JavaCompiler) -> anyhow::Result<()> {
let reflector = Reflector::new(compiler.configuration());
let (java_interface_ref, java_interface_span) = self.java_interface()?;
let java_interface_info =
reflector.reflect(&java_interface_ref.name, java_interface_span)?;

let shim_name = format!("Shim${}", java_interface_info.name.to_dollar_name());
let java_file = compiler.java_file("duchess", &shim_name);
ShimWriter::new(
&mut java_file.src_writer()?,
&shim_name,
&java_interface_info,
)
.emit_shim_class()?;

compiler.compile_to_rs_file(&java_file)?;

eprintln!("compiled to {}", java_file.rs_path.display());

Ok(())
}

fn java_interface(&self) -> anyhow::Result<(ClassRef, Span)> {
let Some((_, trait_path, _)) = &self.item.trait_ else {
return Err(syn::Error::new_spanned(&self.item, "expected an impl of a trait").into());
};
let class_ref = ClassRef::from(&self.item.generics, trait_path)?;
Ok((class_ref, trait_path.span()))
}
}
Loading

0 comments on commit eba2cee

Please sign in to comment.