public_api.rs 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. use std::{
  2. fmt::Write as _,
  3. fs::{read_to_string, File},
  4. io::Write as _,
  5. path::Path,
  6. };
  7. use anyhow::{bail, Context as _, Result};
  8. use cargo_metadata::{Metadata, Package};
  9. use clap::Parser;
  10. use dialoguer::{theme::ColorfulTheme, Confirm};
  11. use diff::{lines, Result as Diff};
  12. use xtask::Errors;
  13. #[derive(Debug, Parser)]
  14. pub struct Options {
  15. /// Bless new API changes.
  16. #[clap(long)]
  17. pub bless: bool,
  18. }
  19. pub fn public_api(options: Options, metadata: Metadata) -> Result<()> {
  20. let toolchain = "nightly";
  21. let Options { bless } = options;
  22. if !rustup_toolchain::is_installed(toolchain)? {
  23. if Confirm::with_theme(&ColorfulTheme::default())
  24. .with_prompt("No nightly toolchain detected. Would you like to install one?")
  25. .interact()?
  26. {
  27. rustup_toolchain::install(toolchain)?;
  28. } else {
  29. bail!("nightly toolchain not installed")
  30. }
  31. }
  32. let Metadata {
  33. workspace_root,
  34. packages,
  35. ..
  36. } = metadata;
  37. let errors: Vec<_> = packages
  38. .into_iter()
  39. .map(|Package { name, publish, .. }| {
  40. if matches!(publish, Some(publish) if publish.is_empty()) {
  41. Ok(())
  42. } else {
  43. let diff = check_package_api(&name, toolchain, bless, workspace_root.as_std_path())
  44. .with_context(|| format!("{name} failed to check public API"))?;
  45. if diff.is_empty() {
  46. Ok(())
  47. } else {
  48. Err(anyhow::anyhow!(
  49. "{name} public API changed; re-run with --bless. diff:\n{diff}"
  50. ))
  51. }
  52. }
  53. })
  54. .filter_map(|result| match result {
  55. Ok(()) => None,
  56. Err(err) => Some(err),
  57. })
  58. .collect();
  59. if errors.is_empty() {
  60. Ok(())
  61. } else {
  62. Err(Errors::new(errors).into())
  63. }
  64. }
  65. fn check_package_api(
  66. package: &str,
  67. toolchain: &str,
  68. bless: bool,
  69. workspace_root: &Path,
  70. ) -> Result<String> {
  71. let path = workspace_root
  72. .join("xtask")
  73. .join("public-api")
  74. .join(package)
  75. .with_extension("txt");
  76. let rustdoc_json = rustdoc_json::Builder::default()
  77. .toolchain(toolchain)
  78. .package(package)
  79. .all_features(true)
  80. .build()
  81. .context("rustdoc_json::Builder::build")?;
  82. let public_api = public_api::Builder::from_rustdoc_json(rustdoc_json)
  83. .build()
  84. .context("public_api::Builder::build")?;
  85. if bless {
  86. let mut output =
  87. File::create(&path).with_context(|| format!("error creating {}", path.display()))?;
  88. write!(&mut output, "{}", public_api)
  89. .with_context(|| format!("error writing {}", path.display()))?;
  90. }
  91. let current_api =
  92. read_to_string(&path).with_context(|| format!("error reading {}", path.display()))?;
  93. Ok(lines(&public_api.to_string(), &current_api)
  94. .into_iter()
  95. .fold(String::new(), |mut buf, diff| {
  96. match diff {
  97. Diff::Both(..) => (),
  98. Diff::Right(line) => writeln!(&mut buf, "-{}", line).unwrap(),
  99. Diff::Left(line) => writeln!(&mut buf, "+{}", line).unwrap(),
  100. };
  101. buf
  102. }))
  103. }