public_api.rs 3.3 KB

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