use anyhow::Context;
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Package};
use quote::quote;
use std::{
env,
ffi::OsString,
fmt::{Display, Formatter},
path::Path,
process::{Command, Stdio},
};
use syn_select::Selector;
#[derive(Debug)]
struct MissingWorkspace;
impl Display for MissingWorkspace {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("virtual manifests must be configured with [workspace]")
}
}
impl std::error::Error for MissingWorkspace {}
#[derive(Debug, Clone, Default)]
pub struct Expander {
pub features: Vec<String>,
pub all_features: bool,
pub no_default_features: bool,
pub tests: bool,
pub release: bool,
pub unstable_flags: Vec<String>,
pub manifest_path: Option<String>,
}
impl Expander {
pub fn add_feature(mut self, s: impl Into<String>) -> Self {
self.features.push(s.into());
self
}
pub fn add_unstable_flag(mut self, s: impl Into<String>) -> Self {
self.unstable_flags.push(s.into());
self
}
pub fn with_manifest(mut self, s: impl Into<String>) -> Self {
self.manifest_path = Some(s.into());
self
}
pub fn with_tests(mut self) -> Self {
self.tests = true;
self
}
pub fn with_all_features(mut self) -> Self {
self.all_features = true;
self
}
pub fn with_no_default_features(mut self) -> Self {
self.no_default_features = true;
self
}
pub fn with_release(mut self) -> Self {
self.release = true;
self
}
pub fn expand(&self, package: impl AsRef<str>) -> anyhow::Result<String> {
let package = package.as_ref();
let pkg = self.find_package(package)?;
let manifest_path = pkg.manifest_path.to_string();
match self.run_cargo(manifest_path) {
res @ Ok(_) => res,
Err(err) => {
match err.downcast::<MissingWorkspace>() {
Ok(_) => {
let tmp =
tempdir::TempDir::new(&format!("dep-expand-{}", pkg.name)).unwrap();
let dep_path = pkg
.manifest_path
.parent()
.context("Failed to find dependency dir")?;
let package_dir = dep_path.file_name().unwrap();
let tmp_package_dir = tmp.path().join(package_dir);
fs_extra::dir::copy(
dep_path,
tmp.path(),
&fs_extra::dir::CopyOptions::default(),
)?;
self.run_cargo(tmp_package_dir.join("Cargo.toml"))
}
Err(err) => Err(err),
}
}
}
}
fn run_cargo(&self, manifest_path: impl AsRef<Path>) -> anyhow::Result<String> {
let mut builder = tempfile::Builder::new();
builder.prefix("dep-expand");
let outdir = builder.tempdir().expect("failed to create tmp file");
let outfile_path = outdir.path().join("expanded");
let mut cmd = Command::new(cargo_binary());
cmd.arg("rustc");
if self.tests {
cmd.arg("--profile=test");
} else {
cmd.arg("--profile=check");
}
if self.release {
cmd.arg("--release");
}
if !self.features.is_empty() {
cmd.arg("--features").arg(self.features.join(" "));
}
if self.all_features {
cmd.arg("--all-features");
}
if self.no_default_features {
cmd.arg("--no-default-features");
}
cmd.arg("--lib")
.arg("--manifest-path")
.arg(manifest_path.as_ref());
for unstable_flag in &self.unstable_flags {
cmd.arg("-Z");
cmd.arg(unstable_flag);
}
cmd.arg("--")
.arg("-o")
.arg(&outfile_path)
.arg("-Zunstable-options")
.arg("--pretty=expanded");
let output = cmd.stderr(Stdio::piped()).spawn()?.wait_with_output()?;
let err = String::from_utf8_lossy(&output.stderr);
if err.starts_with("error: failed to parse manifest at")
&& err
.trim_end()
.ends_with("virtual manifests must be configured with [workspace]")
{
return Err(MissingWorkspace {}.into());
}
let content = std::fs::read_to_string(&outfile_path)?;
if content.is_empty() {
anyhow::bail!("ERROR: rustc produced no expanded output");
}
Ok(content)
}
pub fn expand_path(&self, package: impl AsRef<str>, path: Selector) -> anyhow::Result<String> {
let content = self.expand(package)?;
filter(content, path)
}
fn get_metadata(&self) -> anyhow::Result<Metadata> {
Ok(MetadataCommand::new()
.manifest_path(self.manifest_path.as_deref().unwrap_or(&format!(
"{}/Cargo.toml",
env::var("CARGO_MANIFEST_DIR").expect("No Manifest found")
)))
.features(CargoOpt::AllFeatures)
.exec()?)
}
fn find_package(&self, name: impl AsRef<str>) -> anyhow::Result<Package> {
let name = name.as_ref();
let metadata = self.get_metadata()?;
metadata
.packages
.into_iter()
.find(|pkg| pkg.name == name)
.context(format!("No package found with matching name: `{}`", name))
}
}
fn cargo_binary() -> OsString {
env::var_os("CARGO").unwrap_or_else(|| "cargo".to_owned().into())
}
pub fn filter(mut content: String, filter: Selector) -> anyhow::Result<String> {
let mut syntax_tree = syn::parse_file(&content)?;
syntax_tree.shebang = None;
syntax_tree.attrs.clear();
syntax_tree.items = filter.apply_to(&syntax_tree);
content = quote!(#syntax_tree).to_string();
Ok(content)
}