cargo/ops/
cargo_doc.rs
1use crate::core::compiler::{Compilation, CompileKind};
2use crate::core::{shell::Verbosity, Shell, Workspace};
3use crate::ops;
4use crate::util::context::{GlobalContext, PathAndArgs};
5use crate::util::CargoResult;
6use anyhow::{bail, Error};
7use std::path::Path;
8use std::path::PathBuf;
9use std::process::Command;
10use std::str::FromStr;
11
12#[derive(Debug, Default, Clone)]
16pub enum OutputFormat {
17 #[default]
18 Html,
19 Json,
20}
21
22impl OutputFormat {
23 pub const POSSIBLE_VALUES: [&'static str; 2] = ["html", "json"];
24}
25
26impl FromStr for OutputFormat {
27 type Err = Error;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 match s {
32 "json" => Ok(OutputFormat::Json),
33 "html" => Ok(OutputFormat::Html),
34 _ => bail!(
35 "supported values for --output-format are `json` and `html`, \
36 but `{}` is unknown",
37 s
38 ),
39 }
40 }
41}
42
43#[derive(Debug)]
45pub struct DocOptions {
46 pub open_result: bool,
48 pub output_format: OutputFormat,
50 pub compile_opts: ops::CompileOptions,
52}
53
54pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> {
56 let compilation = ops::compile(ws, &options.compile_opts)?;
57
58 if options.open_result {
59 let name = &compilation.root_crate_names.get(0).ok_or_else(|| {
60 anyhow::anyhow!(
61 "cannot open specified crate's documentation: no documentation generated"
62 )
63 })?;
64 let kind = options.compile_opts.build_config.single_requested_kind()?;
65
66 let path = path_by_output_format(&compilation, &kind, &name, &options.output_format);
67
68 if path.exists() {
69 let config_browser = {
70 let cfg: Option<PathAndArgs> = ws.gctx().get("doc.browser")?;
71 cfg.map(|path_args| (path_args.path.resolve_program(ws.gctx()), path_args.args))
72 };
73 let mut shell = ws.gctx().shell();
74 let link = shell.err_file_hyperlink(&path);
75 shell.status("Opening", format!("{link}{}{link:#}", path.display()))?;
76 open_docs(&path, &mut shell, config_browser, ws.gctx())?;
77 }
78 } else if ws.gctx().shell().verbosity() == Verbosity::Verbose {
79 for name in &compilation.root_crate_names {
80 for kind in &options.compile_opts.build_config.requested_kinds {
81 let path =
82 path_by_output_format(&compilation, &kind, &name, &options.output_format);
83 if path.exists() {
84 let mut shell = ws.gctx().shell();
85 let link = shell.err_file_hyperlink(&path);
86 shell.status("Generated", format!("{link}{}{link:#}", path.display()))?;
87 }
88 }
89 }
90 } else {
91 let mut output = compilation.root_crate_names.iter().flat_map(|name| {
92 options
93 .compile_opts
94 .build_config
95 .requested_kinds
96 .iter()
97 .map(|kind| path_by_output_format(&compilation, kind, name, &options.output_format))
98 .filter(|path| path.exists())
99 });
100 if let Some(first_path) = output.next() {
101 let remaining = output.count();
102 let remaining = match remaining {
103 0 => "".to_owned(),
104 1 => " and 1 other file".to_owned(),
105 n => format!(" and {n} other files"),
106 };
107
108 let mut shell = ws.gctx().shell();
109 let link = shell.err_file_hyperlink(&first_path);
110 shell.status(
111 "Generated",
112 format!("{link}{}{link:#}{remaining}", first_path.display(),),
113 )?;
114 }
115 }
116
117 Ok(())
118}
119
120fn path_by_output_format(
121 compilation: &Compilation<'_>,
122 kind: &CompileKind,
123 name: &str,
124 output_format: &OutputFormat,
125) -> PathBuf {
126 if matches!(output_format, OutputFormat::Json) {
127 compilation.root_output[kind]
128 .with_file_name("doc")
129 .join(format!("{}.json", name))
130 } else {
131 compilation.root_output[kind]
132 .with_file_name("doc")
133 .join(name)
134 .join("index.html")
135 }
136}
137
138fn open_docs(
139 path: &Path,
140 shell: &mut Shell,
141 config_browser: Option<(PathBuf, Vec<String>)>,
142 gctx: &GlobalContext,
143) -> CargoResult<()> {
144 let browser =
145 config_browser.or_else(|| Some((PathBuf::from(gctx.get_env_os("BROWSER")?), Vec::new())));
146
147 match browser {
148 Some((browser, initial_args)) => {
149 if let Err(e) = Command::new(&browser).args(initial_args).arg(path).status() {
150 shell.warn(format!(
151 "Couldn't open docs with {}: {}",
152 browser.to_string_lossy(),
153 e
154 ))?;
155 }
156 }
157 None => {
158 if let Err(e) = opener::open(&path) {
159 let e = e.into();
160 crate::display_warning_with_error("couldn't open docs", &e, shell);
161 }
162 }
163 };
164
165 Ok(())
166}