#[cfg(feature = "source-parsing")]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
#[cfg(feature = "source-parsing")]
use Internals::CollectSourcesArgs;
use Internals::generator::{Generator, RenderConfig, SourceConfig};
use Internals::multi_crate::{MultiCrateGenerator, MultiCrateParser};
use Internals::parser::Parser as InternalParser;
#[cfg(feature = "source-parsing")]
use Internals::source::find_source_dir;
use Internals::{Cargo, Command as CliCommand, DocsArgs, GenerateArgs};
use cargo_docs_md as Internals;
#[cfg(feature = "trace")]
use cargo_docs_md::logger::Logger;
#[cfg(feature = "source-parsing")]
use cargo_docs_md::source::{CollectOptions, SourceCollector};
use clap::Parser;
use miette::{IntoDiagnostic, Result, miette};
fn main() -> Result<()> {
miette::set_panic_hook();
let Cargo::DocsMd(cli) = Cargo::parse();
#[cfg(feature = "trace")]
Logger::init_logging(cli.log_level, cli.log_file.as_ref())?;
if let Some(command) = cli.command {
return match command {
CliCommand::Docs(args) => run_docs_command(args),
#[cfg(feature = "source-parsing")]
CliCommand::CollectSources(args) => run_collect_sources(args),
};
}
run_generate(&cli.args)
}
fn run_docs_command(args: DocsArgs) -> Result<()> {
check_nightly_toolchain()?;
if args.clean {
eprintln!("Running cargo clean...");
let status = Command::new("cargo")
.arg("clean")
.status()
.into_diagnostic()?;
if !status.success() {
return Err(miette!("cargo clean failed"));
}
}
let primary_crate = args.primary_crate.or_else(detect_crate_name);
eprintln!("Building rustdoc JSON (this may take a while)...");
let mut cargo_cmd = Command::new("cargo");
cargo_cmd.arg("+nightly").arg("doc");
if args.exclude_private {
cargo_cmd.env("RUSTDOCFLAGS", "-Z unstable-options --output-format json");
} else {
cargo_cmd.env(
"RUSTDOCFLAGS",
"-Z unstable-options --output-format json --document-private-items",
);
}
for arg in &args.cargo_args {
cargo_cmd.arg(arg);
}
let status = cargo_cmd.status().into_diagnostic()?;
if !status.success() {
return Err(miette!(
"cargo doc failed. Make sure nightly toolchain is installed:\n rustup toolchain install nightly"
));
}
eprintln!("Rustdoc JSON generated in target/doc/");
let generate_args = GenerateArgs {
path: None,
dir: Some(PathBuf::from("target/doc")),
no_mdbook: args.no_mdbook,
no_search_index: args.no_search_index,
primary_crate,
output: args.output,
format: args.format,
exclude_private: args.exclude_private,
include_blanket_impls: args.include_blanket_impls,
toc_threshold: args.toc_threshold,
no_quick_reference: args.no_quick_reference,
no_group_impls: args.no_group_impls,
hide_trivial_derives: args.hide_trivial_derives,
no_method_anchors: args.no_method_anchors,
source_locations: args.source_locations,
full_method_docs: args.full_method_docs,
};
run_generate(&generate_args)
}
fn build_render_config(args: &GenerateArgs) -> RenderConfig {
#[cfg(feature = "source-parsing")]
let source_dir = find_source_dir(Path::new("."));
#[cfg(not(feature = "source-parsing"))]
let source_dir: Option<PathBuf> = None;
RenderConfig {
toc_threshold: args.toc_threshold,
quick_reference: !args.no_quick_reference,
group_impls: !args.no_group_impls,
hide_trivial_derives: args.hide_trivial_derives,
method_anchors: !args.no_method_anchors,
full_method_docs: args.full_method_docs,
include_source: SourceConfig {
source_locations: args.source_locations,
source_dir,
..SourceConfig::default()
},
}
}
fn run_generate(args: &GenerateArgs) -> Result<()> {
if let Some(dir) = &args.dir {
eprintln!(
"Scanning directory for rustdoc JSON files: {}",
dir.display()
);
let crates = MultiCrateParser::parse_directory(dir)?;
eprintln!(
"Found {} crates: {}",
crates.len(),
crates
.names()
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
);
let config = build_render_config(args);
let generator = MultiCrateGenerator::new(&crates, args, config);
generator.generate()?;
println!(
"Documentation generated successfully in '{}'",
args.output.display()
);
return Ok(());
}
let path = args
.path
.as_ref()
.expect("clap ensures path or dir is provided");
let krate = InternalParser::parse_file(path)?;
let config = build_render_config(args);
Generator::new(&krate, args, config)?.generate()?;
println!(
"Documentation generated successfully in '{}'",
args.output.display()
);
Ok(())
}
fn check_nightly_toolchain() -> Result<()> {
let output = Command::new("rustup")
.args(["toolchain", "list"])
.output()
.into_diagnostic()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.lines().any(|line| line.starts_with("nightly")) {
return Err(miette!(
"Rust nightly toolchain is not installed.\n\n\
rustdoc JSON output requires the nightly toolchain.\n\
Install it with:\n\n \
rustup toolchain install nightly"
));
}
Ok(())
}
#[cfg(feature = "source-parsing")]
fn detect_crate_name() -> Option<String> {
use cargo_metadata::MetadataCommand;
let metadata = MetadataCommand::new().no_deps().exec().ok()?;
let current_manifest = std::env::current_dir()
.ok()?
.join("Cargo.toml")
.canonicalize()
.ok()?;
for pkg in &metadata.packages {
if let Ok(pkg_manifest) = pkg.manifest_path.canonicalize()
&& pkg_manifest == current_manifest {
eprintln!("Detected primary crate: {}", pkg.name);
return Some(pkg.name.to_string());
}
}
if !metadata.workspace_members.is_empty() {
for pkg in &metadata.packages {
if metadata.workspace_members.contains(&pkg.id) {
eprintln!("Detected primary crate (workspace member): {}", pkg.name);
return Some(pkg.name.to_string());
}
}
}
None
}
#[cfg(not(feature = "source-parsing"))]
fn detect_crate_name() -> Option<String> {
let cargo_toml = std::fs::read_to_string("Cargo.toml").ok()?;
let mut in_package = false;
for line in cargo_toml.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
continue;
}
if trimmed.starts_with('[') {
in_package = false;
continue;
}
if in_package
&& trimmed.starts_with("name")
&& let Some(name) = trimmed
.split('=')
.nth(1)
.map(|s| s.trim().trim_matches('"').trim_matches('\''))
{
eprintln!("Detected primary crate: {name}");
return Some(name.to_string());
}
}
None
}
#[cfg(feature = "source-parsing")]
fn run_collect_sources(args: CollectSourcesArgs) -> Result<()> {
eprintln!("Collecting dependency sources...");
let collector = match &args.manifest_path {
Some(path) => SourceCollector::from_manifest(Some(path)).into_diagnostic()?,
None => SourceCollector::new().into_diagnostic()?,
};
let options = CollectOptions {
include_dev: args.include_dev,
output: args.output,
dry_run: args.dry_run,
minimal_sources: args.minimal_sources,
no_gitignore: args.no_gitignore,
};
let result = collector.collect(&options).into_diagnostic()?;
if args.dry_run {
println!(
"Dry run - would collect {} crates to:",
result.crates_collected
);
println!(" {}", result.output_dir.display());
} else {
println!(
"Collected {} crates to '{}'",
result.crates_collected,
result.output_dir.display()
);
}
if !result.skipped.is_empty() {
eprintln!(
"\nSkipped {} crates (not found in registry):",
result.skipped.len()
);
for name in &result.skipped {
eprintln!(" - {name}");
}
}
Ok(())
}