public_api.rs 3.6 KB

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