public_api.rs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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, Target};
  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. /// Bless new API changes.
  19. #[clap(long)]
  20. pub target: Option<String>,
  21. }
  22. pub fn public_api(options: Options, metadata: Metadata) -> Result<()> {
  23. let toolchain = "nightly";
  24. let Options { bless, target } = options;
  25. if !rustup_toolchain::is_installed(toolchain)? {
  26. if Confirm::with_theme(&ColorfulTheme::default())
  27. .with_prompt("No nightly toolchain detected. Would you like to install one?")
  28. .interact()?
  29. {
  30. rustup_toolchain::install(toolchain)?;
  31. } else {
  32. bail!("nightly toolchain not installed")
  33. }
  34. }
  35. let Metadata {
  36. workspace_root,
  37. packages,
  38. ..
  39. } = metadata;
  40. let errors: Vec<_> = packages
  41. .into_iter()
  42. .map(
  43. |Package {
  44. name,
  45. publish,
  46. targets,
  47. ..
  48. }| {
  49. if matches!(publish, Some(publish) if publish.is_empty()) {
  50. Ok(())
  51. } else {
  52. let target = target.as_ref().and_then(|target| {
  53. let proc_macro = targets.iter().any(|Target { kind, .. }| {
  54. kind.iter().any(|kind| kind == "proc-macro")
  55. });
  56. (!proc_macro).then_some(target)
  57. });
  58. let diff = check_package_api(
  59. &name,
  60. toolchain,
  61. target.cloned(),
  62. bless,
  63. workspace_root.as_std_path(),
  64. )
  65. .with_context(|| format!("{name} failed to check public API"))?;
  66. if diff.is_empty() {
  67. Ok(())
  68. } else {
  69. Err(anyhow::anyhow!(
  70. "{name} public API changed; re-run with --bless. diff:\n{diff}"
  71. ))
  72. }
  73. }
  74. },
  75. )
  76. .filter_map(|result| match result {
  77. Ok(()) => None,
  78. Err(err) => Some(err),
  79. })
  80. .collect();
  81. if errors.is_empty() {
  82. Ok(())
  83. } else {
  84. Err(Errors::new(errors).into())
  85. }
  86. }
  87. fn check_package_api(
  88. package: &str,
  89. toolchain: &str,
  90. target: Option<String>,
  91. bless: bool,
  92. workspace_root: &Path,
  93. ) -> Result<String> {
  94. let path = workspace_root
  95. .join("xtask")
  96. .join("public-api")
  97. .join(package)
  98. .with_extension("txt");
  99. let mut builder = rustdoc_json::Builder::default()
  100. .toolchain(toolchain)
  101. .package(package)
  102. .all_features(true);
  103. if let Some(target) = target {
  104. builder = builder.target(target);
  105. }
  106. let rustdoc_json = builder.build().with_context(|| {
  107. format!(
  108. "rustdoc_json::Builder::default().toolchain({}).package({}).build()",
  109. toolchain, package
  110. )
  111. })?;
  112. let public_api = public_api::Builder::from_rustdoc_json(&rustdoc_json)
  113. .build()
  114. .with_context(|| {
  115. format!(
  116. "public_api::Builder::from_rustdoc_json({})::build()",
  117. rustdoc_json.display()
  118. )
  119. })?;
  120. if bless {
  121. let mut output =
  122. File::create(&path).with_context(|| format!("error creating {}", path.display()))?;
  123. write!(&mut output, "{}", public_api)
  124. .with_context(|| format!("error writing {}", path.display()))?;
  125. }
  126. let current_api =
  127. read_to_string(&path).with_context(|| format!("error reading {}", path.display()))?;
  128. Ok(lines(&public_api.to_string(), &current_api)
  129. .into_iter()
  130. .fold(String::new(), |mut buf, diff| {
  131. match diff {
  132. Diff::Both(..) => (),
  133. Diff::Right(line) => writeln!(&mut buf, "-{}", line).unwrap(),
  134. Diff::Left(line) => writeln!(&mut buf, "+{}", line).unwrap(),
  135. };
  136. buf
  137. }))
  138. }