use crate::codegen::dumper::Dumper;
use crate::codegen::ConfigDumpContent;
use crate::command_args;
use crate::library::commands::command_runner::{execute_command, ExecuteCommandOptions};
use crate::utils::crate_name::CrateName;
use anyhow::{bail, Context, Result};
use itertools::Itertools;
use lazy_static::lazy_static;
use log::{debug, info};
use regex::{Captures, Regex};
use std::borrow::Cow;
use std::env;
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub(super) fn run(
rust_crate_dir: &Path,
interest_crate_name: Option<&CrateName>,
dumper: &Dumper,
features: Option<&[String]>,
) -> Result<syn::File> {
let text = run_with_frb_aware(rust_crate_dir, interest_crate_name, features)?;
(dumper.with_content(ConfigDumpContent::Source)).dump_str("cargo_expand.rs", &text)?;
Ok(syn::parse_file(&text)?)
}
fn run_with_frb_aware(
rust_crate_dir: &Path,
interest_crate_name: Option<&CrateName>,
features: Option<&[String]>,
) -> Result<String> {
Ok(decode_macro_frb_encoded_comments(&run_raw(
rust_crate_dir,
interest_crate_name,
"--cfg frb_expand",
true,
features,
)?)
.into_owned())
}
fn decode_macro_frb_encoded_comments(code: &str) -> Cow<'_, str> {
lazy_static! {
static ref PATTERN: Regex =
Regex::new(r##"#\[doc =[\s\n]*"frb_encoded\(([\s\S]*?)\)"\]"##).unwrap();
}
PATTERN.replace_all(code, |captures: &Captures| {
let hex_str = captures.get(1).unwrap().as_str();
let decoded_str = String::from_utf8(hex::decode(hex_str).unwrap()).unwrap();
decoded_str.to_string()
})
}
#[allow(clippy::vec_init_then_push)]
fn run_raw(
rust_crate_dir: &Path,
interest_crate_name: Option<&CrateName>,
extra_rustflags: &str,
allow_auto_install: bool,
features: Option<&[String]>,
) -> Result<String> {
debug!("Running cargo expand in '{rust_crate_dir:?}'");
let args_choosing_crate = if let Some(interest_crate_name) = interest_crate_name {
vec!["-p", interest_crate_name.raw()]
} else {
vec![]
};
let args_features: Vec<PathBuf> = features
.unwrap_or_default()
.iter()
.flat_map(|feature| vec!["--features", feature])
.map(PathBuf::from_str)
.try_collect()?;
let args = command_args!(
"expand",
"--lib",
"--theme=none",
"--ugly",
*args_choosing_crate,
*args_features
);
let extra_env = [(
"RUSTFLAGS".to_owned(),
env::var("RUSTFLAGS").map(|x| x + " ").unwrap_or_default() + extra_rustflags,
)]
.into();
let output = execute_command(
"cargo",
&args,
Some(rust_crate_dir),
Some(ExecuteCommandOptions {
envs: Some(extra_env),
..Default::default()
}),
)
.with_context(|| format!("Could not expand rust code at path {rust_crate_dir:?}"))?;
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
if stdout.is_empty() {
if stderr.contains("no such command: `expand`") && allow_auto_install {
info!("Cargo expand is not installed. Automatically install and re-run.");
install_cargo_expand()?;
return run_raw(
rust_crate_dir,
interest_crate_name,
extra_rustflags,
false,
features,
);
}
bail!("cargo expand returned empty output");
}
Ok(stdout.lines().skip(1).join("\n"))
}
fn install_cargo_expand() -> Result<()> {
execute_command(
"cargo",
&vec!["install".into(), "cargo-expand".into()],
None,
None,
)?;
Ok(())
}