You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aya/xtask/src/public_api.rs

152 lines
4.4 KiB
Rust

use std::{
fmt::Write as _,
fs::{read_to_string, File},
io::Write as _,
path::Path,
};
use anyhow::{bail, Context as _, Result};
use cargo_metadata::{Metadata, Package, Target};
use clap::Parser;
use dialoguer::{theme::ColorfulTheme, Confirm};
use diff::{lines, Result as Diff};
use xtask::Errors;
#[derive(Debug, Parser)]
pub struct Options {
/// Bless new API changes.
#[clap(long)]
pub bless: bool,
/// Bless new API changes.
#[clap(long)]
pub target: Option<String>,
}
pub fn public_api(options: Options, metadata: Metadata) -> Result<()> {
let toolchain = "nightly";
let Options { bless, target } = options;
if !rustup_toolchain::is_installed(toolchain)? {
if Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("No nightly toolchain detected. Would you like to install one?")
.interact()?
{
rustup_toolchain::install(toolchain)?;
} else {
bail!("nightly toolchain not installed")
}
}
let Metadata {
workspace_root,
packages,
..
} = metadata;
let errors: Vec<_> = packages
.into_iter()
.map(
|Package {
name,
publish,
targets,
..
}| {
if matches!(publish, Some(publish) if publish.is_empty()) {
Ok(())
} else {
let target = target.as_ref().and_then(|target| {
let proc_macro = targets.iter().any(|Target { kind, .. }| {
kind.iter().any(|kind| kind == "proc-macro")
});
(!proc_macro).then_some(target)
});
let diff = check_package_api(
&name,
toolchain,
target.cloned(),
bless,
workspace_root.as_std_path(),
)
.with_context(|| format!("{name} failed to check public API"))?;
if diff.is_empty() {
Ok(())
} else {
Err(anyhow::anyhow!(
"{name} public API changed; re-run with --bless. diff:\n{diff}"
))
}
}
},
)
.filter_map(|result| match result {
Ok(()) => None,
Err(err) => Some(err),
})
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(Errors::new(errors).into())
}
}
fn check_package_api(
package: &str,
toolchain: &str,
target: Option<String>,
bless: bool,
workspace_root: &Path,
) -> Result<String> {
let path = workspace_root
.join("xtask")
.join("public-api")
.join(package)
.with_extension("txt");
let mut builder = rustdoc_json::Builder::default()
.toolchain(toolchain)
.package(package)
.all_features(true);
if let Some(target) = target {
builder = builder.target(target);
}
let rustdoc_json = builder.build().with_context(|| {
format!(
"rustdoc_json::Builder::default().toolchain({}).package({}).build()",
toolchain, package
)
})?;
let public_api = public_api::Builder::from_rustdoc_json(&rustdoc_json)
.build()
.with_context(|| {
format!(
"public_api::Builder::from_rustdoc_json({})::build()",
rustdoc_json.display()
)
})?;
if bless {
let mut output =
File::create(&path).with_context(|| format!("error creating {}", path.display()))?;
write!(&mut output, "{}", public_api)
.with_context(|| format!("error writing {}", path.display()))?;
}
let current_api =
read_to_string(&path).with_context(|| format!("error reading {}", path.display()))?;
Ok(lines(&public_api.to_string(), &current_api)
.into_iter()
.fold(String::new(), |mut buf, diff| {
match diff {
Diff::Both(..) => (),
Diff::Right(line) => writeln!(&mut buf, "-{}", line).unwrap(),
Diff::Left(line) => writeln!(&mut buf, "+{}", line).unwrap(),
};
buf
}))
}