1use anyhow::{Context, Result, bail};
7use std::path::Path;
8use std::process::Command;
9
10pub const REQUIRED_TOOLCHAIN: &str = "nightly-2025-06-23";
12
13pub async fn validate_toolchain() -> Result<()> {
15 let output = Command::new("rustup")
16 .args(["toolchain", "list"])
17 .output()
18 .context("Failed to run rustup toolchain list")?;
19
20 if !output.status.success() {
21 bail!("Failed to check available toolchains");
22 }
23
24 let toolchains = String::from_utf8_lossy(&output.stdout);
25 if !toolchains.contains(REQUIRED_TOOLCHAIN) {
26 bail!(
27 "Required toolchain {} is not installed. Please run: rustup toolchain install {}",
28 REQUIRED_TOOLCHAIN,
29 REQUIRED_TOOLCHAIN
30 );
31 }
32
33 tracing::debug!("Validated toolchain {} is available", REQUIRED_TOOLCHAIN);
34 Ok(())
35}
36
37pub async fn test_rustdoc_json() -> Result<()> {
39 validate_toolchain().await?;
41
42 let temp_dir =
44 tempfile::tempdir().context("Failed to create temporary directory for testing")?;
45
46 let test_file = temp_dir.path().join("lib.rs");
47 std::fs::write(&test_file, "//! Test crate\npub fn test() {}")
48 .context("Failed to create test file")?;
49
50 let test_file_str = test_file
51 .to_str()
52 .ok_or_else(|| anyhow::anyhow!("Test file path contains invalid UTF-8"))?;
53
54 tracing::debug!(
55 "Testing rustdoc JSON generation with {}",
56 REQUIRED_TOOLCHAIN
57 );
58
59 let output = Command::new("rustdoc")
61 .args([
62 &format!("+{REQUIRED_TOOLCHAIN}"),
63 "-Z",
64 "unstable-options",
65 "--output-format",
66 "json",
67 "--crate-name",
68 "test",
69 test_file_str,
70 ])
71 .output()
72 .context("Failed to run rustdoc")?;
73
74 if !output.status.success() {
75 let stderr = String::from_utf8_lossy(&output.stderr);
76 bail!("JSON generation failed: {}", stderr);
77 }
78
79 tracing::debug!("Successfully tested rustdoc JSON generation");
80 Ok(())
81}
82
83pub async fn get_rustdoc_version() -> Result<String> {
85 let output = Command::new("rustdoc")
86 .arg("--version")
87 .output()
88 .context("Failed to run rustdoc --version")?;
89
90 if !output.status.success() {
91 bail!("rustdoc command failed");
92 }
93
94 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
95}
96
97pub async fn run_cargo_rustdoc_json(source_path: &Path, package: Option<&str>) -> Result<()> {
99 validate_toolchain().await?;
100
101 let log_msg = match package {
102 Some(pkg) => format!(
103 "Running cargo rustdoc with JSON output for package {} in {}",
104 pkg,
105 source_path.display()
106 ),
107 None => format!(
108 "Running cargo rustdoc with JSON output in {}",
109 source_path.display()
110 ),
111 };
112 tracing::debug!("{}", log_msg);
113
114 let mut base_args = vec![format!("+{}", REQUIRED_TOOLCHAIN), "rustdoc".to_string()];
115
116 if let Some(pkg) = package {
118 base_args.push("-p".to_string());
119 base_args.push(pkg.to_string());
120 }
121
122 let rustdoc_args = vec![
124 "--all-features".to_string(),
125 "--".to_string(),
126 "--output-format".to_string(),
127 "json".to_string(),
128 "-Z".to_string(),
129 "unstable-options".to_string(),
130 ];
131
132 let mut args = base_args.clone();
134 args.extend_from_slice(&rustdoc_args);
135
136 let output = Command::new("cargo")
137 .args(&args)
138 .current_dir(source_path)
139 .output()
140 .context("Failed to run cargo rustdoc")?;
141
142 if !output.status.success() {
143 let stderr = String::from_utf8_lossy(&output.stderr);
144
145 if stderr.contains("extra arguments to `rustdoc` can only be passed to one target") {
147 tracing::debug!("Multiple targets detected, retrying with --lib flag");
148
149 let mut args_with_lib = base_args;
151 args_with_lib.push("--lib".to_string());
152 args_with_lib.extend_from_slice(&rustdoc_args);
153
154 let output_with_lib = Command::new("cargo")
155 .args(&args_with_lib)
156 .current_dir(source_path)
157 .output()
158 .context("Failed to run cargo rustdoc with --lib")?;
159
160 if !output_with_lib.status.success() {
161 let stderr_with_lib = String::from_utf8_lossy(&output_with_lib.stderr);
162
163 if stderr_with_lib.contains("no library targets found") {
164 bail!("This is a binary-only package");
165 }
166
167 bail!("Failed to generate documentation: {}", stderr_with_lib);
168 }
169
170 return Ok(());
172 }
173
174 if stderr.contains("could not find `Cargo.toml` in") || stderr.contains("workspace") {
176 bail!(
177 "This appears to be a workspace. Please use workspace member caching instead of trying to cache the root workspace."
178 );
179 }
180
181 bail!("Failed to generate documentation: {}", stderr);
182 }
183
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[tokio::test]
192 async fn test_get_rustdoc_version() {
193 let result = get_rustdoc_version().await;
195 assert!(result.is_ok() || result.is_err());
198 }
199
200 #[tokio::test]
201 async fn test_validate_toolchain() {
202 let result = validate_toolchain().await;
204 assert!(result.is_ok() || result.is_err());
207 }
208}