mirror of https://github.com/zcash/wallet.git
335 lines
12 KiB
Rust
335 lines
12 KiB
Rust
use std::env;
|
|
use std::error::Error;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use clap::{Command, CommandFactory, ValueEnum};
|
|
use clap_complete::{Shell, generate_to};
|
|
use clap_mangen::Man;
|
|
use flate2::{Compression, write::GzEncoder};
|
|
use i18n_embed::unic_langid::LanguageIdentifier;
|
|
use quote::ToTokens;
|
|
|
|
const JSON_RPC_METHODS_RS: &str = "src/components/json_rpc/methods.rs";
|
|
|
|
mod i18n {
|
|
include!("src/i18n.rs");
|
|
}
|
|
mod zallet {
|
|
include!("src/cli.rs");
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! fl {
|
|
($message_id:literal) => {{
|
|
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id)
|
|
}};
|
|
|
|
($message_id:literal, $($args:expr),* $(,)?) => {{
|
|
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)
|
|
}};
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
println!("cargo::rerun-if-changed=build.rs");
|
|
println!("cargo::rerun-if-changed=src/cli.rs");
|
|
println!("cargo::rerun-if-changed=src/i18n.rs");
|
|
println!("cargo::rerun-if-changed={JSON_RPC_METHODS_RS}");
|
|
|
|
// Expose a cfg option so we can make parts of the CLI conditional on not being built
|
|
// within the buildscript.
|
|
println!("cargo:rustc-cfg=outside_buildscript");
|
|
|
|
let out_dir = match env::var_os("OUT_DIR") {
|
|
None => return Ok(()),
|
|
Some(out_dir) => PathBuf::from(out_dir),
|
|
};
|
|
|
|
// `OUT_DIR` is "intentionally opaque as it is only intended for `rustc` interaction"
|
|
// (https://github.com/rust-lang/cargo/issues/9858). Peek into the black box and use
|
|
// it to figure out where the target directory is.
|
|
let target_dir = out_dir
|
|
.ancestors()
|
|
.nth(3)
|
|
.expect("should be absolute path")
|
|
.to_path_buf();
|
|
|
|
generate_rpc_openrpc(&out_dir)?;
|
|
|
|
// Generate the completions in English, because these aren't easily localizable.
|
|
i18n::load_languages(&[]);
|
|
Cli::build().generate_completions(&target_dir.join("completions"))?;
|
|
|
|
// Generate manpages for all supported languages.
|
|
let manpage_dir = target_dir.join("manpages");
|
|
for lang_dir in fs::read_dir("./i18n")? {
|
|
let lang_dir = lang_dir?.file_name();
|
|
let lang_dir = lang_dir.to_str().expect("should be valid Unicode");
|
|
println!("cargo::rerun-if-changed=i18n/{lang_dir}/zallet.ftl");
|
|
|
|
let lang: LanguageIdentifier = lang_dir
|
|
.parse()
|
|
.expect("should be valid language identifier");
|
|
|
|
// Render the manpages into the correct folder structure, so that local checks can
|
|
// be performed with `man -M target/debug/manpages BINARY_NAME`.
|
|
let mut out_dir = if lang.language.as_str() == "en" {
|
|
manpage_dir.clone()
|
|
} else {
|
|
let mut lang_str = lang.language.as_str().to_owned();
|
|
if let Some(region) = lang.region {
|
|
// Locales for manpages use the POSIX format with underscores.
|
|
lang_str += "_";
|
|
lang_str += region.as_str();
|
|
}
|
|
manpage_dir.join(lang_str)
|
|
};
|
|
out_dir.push("man1");
|
|
|
|
i18n::load_languages(&[lang]);
|
|
Cli::build().generate_manpages(&out_dir)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Cli {
|
|
zallet: Command,
|
|
}
|
|
|
|
impl Cli {
|
|
fn build() -> Self {
|
|
Self {
|
|
zallet: zallet::EntryPoint::command(),
|
|
}
|
|
}
|
|
|
|
fn generate_completions(&mut self, out_dir: &Path) -> io::Result<()> {
|
|
fs::create_dir_all(out_dir)?;
|
|
|
|
for &shell in Shell::value_variants() {
|
|
generate_to(shell, &mut self.zallet, "zallet", out_dir)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_manpages(self, out_dir: &Path) -> io::Result<()> {
|
|
fs::create_dir_all(out_dir)?;
|
|
|
|
fn generate_manpage(
|
|
out_dir: &Path,
|
|
name: &str,
|
|
cmd: Command,
|
|
custom: impl FnOnce(&Man, &mut GzEncoder<fs::File>) -> io::Result<()>,
|
|
) -> io::Result<()> {
|
|
let file = fs::File::create(out_dir.join(format!("{}.1.gz", name)))?;
|
|
let mut w = GzEncoder::new(file, Compression::best());
|
|
|
|
let man = Man::new(cmd);
|
|
man.render_title(&mut w)?;
|
|
man.render_name_section(&mut w)?;
|
|
man.render_synopsis_section(&mut w)?;
|
|
man.render_description_section(&mut w)?;
|
|
man.render_options_section(&mut w)?;
|
|
custom(&man, &mut w)?;
|
|
man.render_version_section(&mut w)?;
|
|
man.render_authors_section(&mut w)
|
|
}
|
|
|
|
generate_manpage(
|
|
out_dir,
|
|
"zallet",
|
|
self.zallet
|
|
.about(fl!("man-zallet-about"))
|
|
.long_about(fl!("man-zallet-description")),
|
|
|_, _| Ok(()),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn generate_rpc_openrpc(out_dir: &Path) -> Result<(), Box<dyn Error>> {
|
|
// Parse the source file containing the `Rpc` trait.
|
|
let methods_rs = fs::read_to_string(JSON_RPC_METHODS_RS)?;
|
|
let methods_ast = syn::parse_file(&methods_rs)?;
|
|
|
|
let rpc_trait = methods_ast
|
|
.items
|
|
.iter()
|
|
.find_map(|item| match item {
|
|
syn::Item::Trait(item_trait) if item_trait.ident == "Rpc" => Some(item_trait),
|
|
_ => None,
|
|
})
|
|
.expect("present");
|
|
|
|
let mut contents = "#[allow(unused_qualifications)]
|
|
pub(super) static METHODS: ::phf::Map<&str, RpcMethod> = ::phf::phf_map! {
|
|
"
|
|
.to_string();
|
|
|
|
for item in &rpc_trait.items {
|
|
if let syn::TraitItem::Fn(method) = item {
|
|
// Find methods via their `#[method(name = "command")]` attribute.
|
|
let mut command = None;
|
|
method
|
|
.attrs
|
|
.iter()
|
|
.find(|attr| attr.path().is_ident("method"))
|
|
.and_then(|attr| {
|
|
attr.parse_nested_meta(|meta| {
|
|
command = Some(meta.value()?.parse::<syn::LitStr>()?.value());
|
|
Ok(())
|
|
})
|
|
.ok()
|
|
});
|
|
|
|
if let Some(command) = command {
|
|
let module = match &method.sig.output {
|
|
syn::ReturnType::Type(_, ret) => match ret.as_ref() {
|
|
syn::Type::Path(type_path) => type_path.path.segments.first(),
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
}
|
|
.expect("required")
|
|
.ident
|
|
.to_string();
|
|
|
|
let params = method.sig.inputs.iter().filter_map(|arg| match arg {
|
|
syn::FnArg::Receiver(_) => None,
|
|
syn::FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
|
|
syn::Pat::Ident(pat_ident) => {
|
|
let parameter = pat_ident.ident.to_string();
|
|
let rust_ty = pat_type.ty.as_ref();
|
|
|
|
// If we can determine the parameter's optionality, do so.
|
|
let (param_ty, required) = match rust_ty {
|
|
syn::Type::Path(type_path) => {
|
|
let is_standalone_ident =
|
|
type_path.path.leading_colon.is_none()
|
|
&& type_path.path.segments.len() == 1;
|
|
let first_segment = &type_path.path.segments[0];
|
|
|
|
if first_segment.ident == "Option" && is_standalone_ident {
|
|
// Strip the `Option<_>` for the schema type.
|
|
let schema_ty = match &first_segment.arguments {
|
|
syn::PathArguments::AngleBracketed(args) => {
|
|
match args.args.first().expect("valid Option") {
|
|
syn::GenericArgument::Type(ty) => ty,
|
|
_ => panic!("Invalid Option"),
|
|
}
|
|
}
|
|
_ => panic!("Invalid Option"),
|
|
};
|
|
(schema_ty, Some(false))
|
|
} else if first_segment.ident == "Vec" {
|
|
// We don't know whether the vec may be empty.
|
|
(rust_ty, None)
|
|
} else {
|
|
(rust_ty, Some(true))
|
|
}
|
|
}
|
|
_ => (rust_ty, Some(true)),
|
|
};
|
|
|
|
// Handle a few conversions we know we need.
|
|
let param_ty = param_ty.to_token_stream().to_string();
|
|
let schema_ty = match param_ty.as_str() {
|
|
"age :: secrecy :: SecretString" => "String".into(),
|
|
_ => param_ty,
|
|
};
|
|
|
|
Some((parameter, schema_ty, required))
|
|
}
|
|
_ => None,
|
|
},
|
|
});
|
|
|
|
contents.push('"');
|
|
contents.push_str(&command);
|
|
contents.push_str("\" => RpcMethod {\n");
|
|
|
|
contents.push_str(" description: \"");
|
|
for attr in method
|
|
.attrs
|
|
.iter()
|
|
.filter(|attr| attr.path().is_ident("doc"))
|
|
{
|
|
if let syn::Meta::NameValue(doc_line) = &attr.meta {
|
|
if let syn::Expr::Lit(docs) = &doc_line.value {
|
|
if let syn::Lit::Str(s) = &docs.lit {
|
|
// Trim the leading space from the doc comment line.
|
|
let line = s.value();
|
|
let trimmed_line = if line.is_empty() { &line } else { &line[1..] };
|
|
|
|
let escaped = trimmed_line.escape_default().collect::<String>();
|
|
|
|
contents.push_str(&escaped);
|
|
contents.push_str("\\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
contents.push_str("\",\n");
|
|
|
|
contents.push_str(" params: |_g| vec![\n");
|
|
for (parameter, schema_ty, required) in params {
|
|
let param_upper = parameter.to_uppercase();
|
|
|
|
contents.push_str(" _g.param::<");
|
|
contents.push_str(&schema_ty);
|
|
contents.push_str(">(\"");
|
|
contents.push_str(¶meter);
|
|
contents.push_str("\", super::");
|
|
contents.push_str(&module);
|
|
contents.push_str("::PARAM_");
|
|
contents.push_str(¶m_upper);
|
|
contents.push_str("_DESC, ");
|
|
match required {
|
|
Some(required) => contents.push_str(&required.to_string()),
|
|
None => {
|
|
// Require a helper const to be present.
|
|
contents.push_str("super::");
|
|
contents.push_str(&module);
|
|
contents.push_str("::PARAM_");
|
|
contents.push_str(¶m_upper);
|
|
contents.push_str("_REQUIRED");
|
|
}
|
|
}
|
|
contents.push_str("),\n");
|
|
}
|
|
contents.push_str(" ],\n");
|
|
|
|
contents.push_str(" result: |g| g.result::<super::");
|
|
contents.push_str(&module);
|
|
contents.push_str("::ResultType>(\"");
|
|
contents.push_str(&command);
|
|
contents.push_str("_result\"),\n");
|
|
|
|
contents.push_str(" deprecated: ");
|
|
contents.push_str(
|
|
&method
|
|
.attrs
|
|
.iter()
|
|
.any(|attr| attr.path().is_ident("deprecated"))
|
|
.to_string(),
|
|
);
|
|
contents.push_str(",\n");
|
|
|
|
contents.push_str("},\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
contents.push_str("};");
|
|
|
|
let rpc_openrpc_path = out_dir.join("rpc_openrpc.rs");
|
|
fs::write(&rpc_openrpc_path, contents)?;
|
|
|
|
Ok(())
|
|
}
|