use anyhow::{Context, Result, bail};
use std::path::Path;
use std::process::Command;
pub const REQUIRED_TOOLCHAIN: &str = "nightly-2025-06-23";
pub async fn validate_toolchain() -> Result<()> {
let output = Command::new("rustup")
.args(["toolchain", "list"])
.output()
.context("Failed to run rustup toolchain list")?;
if !output.status.success() {
bail!("Failed to check available toolchains");
}
let toolchains = String::from_utf8_lossy(&output.stdout);
if !toolchains.contains(REQUIRED_TOOLCHAIN) {
bail!(
"Required toolchain {} is not installed. Please run: rustup toolchain install {}",
REQUIRED_TOOLCHAIN,
REQUIRED_TOOLCHAIN
);
}
tracing::debug!("Validated toolchain {} is available", REQUIRED_TOOLCHAIN);
Ok(())
}
pub async fn test_rustdoc_json() -> Result<()> {
validate_toolchain().await?;
let temp_dir =
tempfile::tempdir().context("Failed to create temporary directory for testing")?;
let test_file = temp_dir.path().join("lib.rs");
std::fs::write(&test_file, "//! Test crate\npub fn test() {}")
.context("Failed to create test file")?;
let test_file_str = test_file
.to_str()
.ok_or_else(|| anyhow::anyhow!("Test file path contains invalid UTF-8"))?;
tracing::debug!(
"Testing rustdoc JSON generation with {}",
REQUIRED_TOOLCHAIN
);
let output = Command::new("rustdoc")
.args([
&format!("+{REQUIRED_TOOLCHAIN}"),
"-Z",
"unstable-options",
"--output-format",
"json",
"--crate-name",
"test",
test_file_str,
])
.output()
.context("Failed to run rustdoc")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("JSON generation failed: {}", stderr);
}
tracing::debug!("Successfully tested rustdoc JSON generation");
Ok(())
}
pub async fn get_rustdoc_version() -> Result<String> {
let output = Command::new("rustdoc")
.arg("--version")
.output()
.context("Failed to run rustdoc --version")?;
if !output.status.success() {
bail!("rustdoc command failed");
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub async fn run_cargo_rustdoc_json(source_path: &Path, package: Option<&str>) -> Result<()> {
validate_toolchain().await?;
let log_msg = match package {
Some(pkg) => format!(
"Running cargo rustdoc with JSON output for package {} in {}",
pkg,
source_path.display()
),
None => format!(
"Running cargo rustdoc with JSON output in {}",
source_path.display()
),
};
tracing::debug!("{}", log_msg);
let mut base_args = vec![format!("+{}", REQUIRED_TOOLCHAIN), "rustdoc".to_string()];
if let Some(pkg) = package {
base_args.push("-p".to_string());
base_args.push(pkg.to_string());
}
let rustdoc_args = vec![
"--all-features".to_string(),
"--".to_string(),
"--output-format".to_string(),
"json".to_string(),
"-Z".to_string(),
"unstable-options".to_string(),
];
let mut args = base_args.clone();
args.extend_from_slice(&rustdoc_args);
let output = Command::new("cargo")
.args(&args)
.current_dir(source_path)
.output()
.context("Failed to run cargo rustdoc")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("extra arguments to `rustdoc` can only be passed to one target") {
tracing::debug!("Multiple targets detected, retrying with --lib flag");
let mut args_with_lib = base_args;
args_with_lib.push("--lib".to_string());
args_with_lib.extend_from_slice(&rustdoc_args);
let output_with_lib = Command::new("cargo")
.args(&args_with_lib)
.current_dir(source_path)
.output()
.context("Failed to run cargo rustdoc with --lib")?;
if !output_with_lib.status.success() {
let stderr_with_lib = String::from_utf8_lossy(&output_with_lib.stderr);
if stderr_with_lib.contains("no library targets found") {
bail!("This is a binary-only package");
}
bail!("Failed to generate documentation: {}", stderr_with_lib);
}
return Ok(());
}
if stderr.contains("could not find `Cargo.toml` in") || stderr.contains("workspace") {
bail!(
"This appears to be a workspace. Please use workspace member caching instead of trying to cache the root workspace."
);
}
bail!("Failed to generate documentation: {}", stderr);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_rustdoc_version() {
let result = get_rustdoc_version().await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_validate_toolchain() {
let result = validate_toolchain().await;
assert!(result.is_ok() || result.is_err());
}
}