I've done a fair amount of code generation in Rust and generally all you need is the proc-macro2 crate and quote. I prefer quote!() over a bare write!() because it lets you take a step away from the nuts and bolts of string interpolation and closer to generating Rust code.
If you want to have nicely formatted code, the prettyplease crate is quite handy because it decouples you from rustfmt, which may not always be installed or could change its formatting style from version-to-version and trigger unnecessary "the generated code needs updating" issues.
Other than those crates, this is all the extra boilerplate I'll need:
Once I've got that in place, I normally stick the code generation into a test which uses ensure_file_contents() to make sure the generated code is up to date.
The proc_macro2::TokenStream type implements Display, so you can always call to_string() on it or write!() the tokens to a file.
The bit doing the formatting and stringifying was just above ensure_file_contents().
The actual code that uses quote!() is pretty complex because I'm taking an *.ungrammar file and turning it into a series of token types (code/generated) and strongly-typed AST nodes (code/generated). It was mostly copied from another, much more complex parser and language server I wrote, but the code for that isn't open-source yet.