Skip to main content

flowey_lib_common/
run_cargo_doc.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Encapsulates the logic of invoking `cargo doc`, taking into account
5//! bits of "global" configuration and dependency management, such as setting
6//! global cargo flags (e.g: --verbose, --locked), ensuring base Rust
7//! dependencies are installed, etc...
8
9use crate::_util::cargo_output;
10use flowey::node::prelude::*;
11use flowey::shell::FloweyCmd;
12use std::collections::BTreeMap;
13#[derive(Serialize, Deserialize)]
14pub struct CargoDocCommands {
15    cmds: Vec<Vec<String>>,
16    cargo_work_dir: PathBuf,
17    no_incremental: bool,
18}
19
20impl CargoDocCommands {
21    /// Execute the doc command(s), returning a path to the built docs
22    /// directory.
23    pub fn run(self, rt: &RustRuntimeServices<'_>) -> anyhow::Result<PathBuf> {
24        self.run_with(rt, |x| x)
25    }
26
27    /// Execute the doc command(s), returning path(s) to the built artifact.
28    ///
29    /// Unlike `run`, this method allows tweaking the build command prior to
30    /// running it (e.g: to add env vars, change the working directory where the
31    /// artifacts will be placed, etc...).
32    pub fn run_with(
33        self,
34        rt: &RustRuntimeServices<'_>,
35        f: impl Fn(FloweyCmd<'_>) -> FloweyCmd<'_>,
36    ) -> anyhow::Result<PathBuf> {
37        let Self {
38            cmds,
39            cargo_work_dir,
40            no_incremental,
41        } = self;
42
43        let out_dir = rt.sh.current_dir();
44        rt.sh.change_dir(cargo_work_dir);
45
46        let mut json = String::new();
47        for mut cmd in cmds {
48            let argv0 = cmd.remove(0);
49            let cmd = flowey::shell_cmd!(rt, "{argv0} {cmd...}");
50            let cmd = if no_incremental {
51                cmd.env("CARGO_INCREMENTAL", "0")
52            } else {
53                cmd
54            };
55            let cmd = f(cmd);
56            json.push_str(&cmd.read()?);
57        }
58        let messages: Vec<cargo_output::Message> = serde_json::Deserializer::from_str(&json)
59            .into_iter()
60            .collect::<Result<_, _>>()?;
61
62        // Find the output directory. Look for a file name like `foo/bar/doc/mycrate/index.html`.
63        let cargo_out_dir = messages
64            .iter()
65            .find_map(|msg| match msg {
66                cargo_output::Message::CompilerArtifact { filenames, .. } => {
67                    filenames.iter().find_map(|filename| {
68                        filename
69                            .file_name()
70                            .is_some_and(|f| f == "index.html")
71                            .then(|| filename.parent().and_then(Path::parent))
72                            .flatten()
73                    })
74                }
75                _ => None,
76            })
77            .context("could not find cargo doc output directory")?;
78
79        anyhow::ensure!(
80            cargo_out_dir.file_name().is_some_and(|name| name == "doc"),
81            "unexpected cargo doc output directory {}",
82            cargo_out_dir.display()
83        );
84
85        let final_dir = out_dir.join("cargo-doc-out");
86        fs_err::rename(cargo_out_dir, &final_dir)?;
87        Ok(final_dir)
88    }
89}
90
91/// Packages that can be documented
92#[derive(Serialize, Deserialize)]
93pub enum DocPackageKind {
94    /// Document an entire workspace workspace (with exclusions)
95    Workspace { exclude: Vec<String> },
96    /// Document a specific crate.
97    Crate(String),
98    /// Document a specific no_std crate.
99    ///
100    /// This is its own variant, as a single `cargo doc` command has issues
101    /// documenting mixed `std` and `no_std` crates.
102    NoStdCrate(String),
103}
104
105/// The "what and how" of packages to documents
106#[derive(Serialize, Deserialize)]
107pub struct DocPackage {
108    /// The thing being documented.
109    pub kind: DocPackageKind,
110    /// Whether to document non-workspace dependencies (i.e: pass `--no-deps`)
111    pub no_deps: bool,
112    /// Whether to document private items (i.e: pass `--document-private-items`)
113    pub document_private_items: bool,
114}
115
116flowey_request! {
117    pub struct Request {
118        pub in_folder: ReadVar<PathBuf>,
119        /// Targets to include in the generated docs.
120        pub packages: Vec<DocPackage>,
121        /// What target-triple things should get documented with.
122        pub target_triple: target_lexicon::Triple,
123        pub cargo_cmd: WriteVar<CargoDocCommands>,
124    }
125}
126
127#[derive(Default)]
128struct ResolvedDocPackages {
129    // where each (bool, bool) represents (no_deps, document_private_items)
130    workspace: Option<(bool, bool)>,
131    exclude: Vec<String>,
132    crates: BTreeMap<(bool, bool), Vec<String>>,
133    crates_no_std: BTreeMap<(bool, bool), Vec<String>>,
134}
135
136new_flow_node!(struct Node);
137
138impl FlowNode for Node {
139    type Request = Request;
140
141    fn imports(ctx: &mut ImportCtx<'_>) {
142        ctx.import::<crate::cfg_cargo_common_flags::Node>();
143        ctx.import::<crate::install_rust::Node>();
144    }
145
146    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
147        let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
148        let flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
149
150        for Request {
151            in_folder,
152            packages,
153            target_triple,
154            cargo_cmd,
155        } in requests
156        {
157            ctx.req(crate::install_rust::Request::InstallTargetTriple(
158                target_triple.clone(),
159            ));
160
161            // figure out what cargo commands we'll need to invoke
162            let mut targets = ResolvedDocPackages::default();
163            for DocPackage {
164                kind,
165                no_deps,
166                document_private_items,
167            } in packages
168            {
169                match kind {
170                    DocPackageKind::Workspace { exclude } => {
171                        if targets.workspace.is_some() {
172                            anyhow::bail!("cannot pass Workspace variant multiple times")
173                        }
174                        targets.exclude.extend(exclude);
175                        targets.workspace = Some((no_deps, document_private_items))
176                    }
177                    DocPackageKind::Crate(name) => targets
178                        .crates
179                        .entry((no_deps, document_private_items))
180                        .or_default()
181                        .push(name),
182                    DocPackageKind::NoStdCrate(name) => targets
183                        .crates_no_std
184                        .entry((no_deps, document_private_items))
185                        .or_default()
186                        .push(name),
187                }
188            }
189
190            let doc_targets = targets;
191
192            ctx.emit_minor_rust_step("construct cargo doc command", |ctx| {
193                let rust_toolchain = rust_toolchain.clone().claim(ctx);
194                let flags = flags.clone().claim(ctx);
195                let in_folder = in_folder.claim(ctx);
196                let write_doc_cmd = cargo_cmd.claim(ctx);
197
198                move |rt| {
199                    let rust_toolchain = rt.read(rust_toolchain);
200                    let flags = rt.read(flags);
201                    let in_folder = rt.read(in_folder);
202
203                    let crate::cfg_cargo_common_flags::Flags {
204                        locked,
205                        verbose,
206                        no_incremental,
207                    } = flags;
208
209                    let mut cmds = Vec::new();
210                    let ResolvedDocPackages {
211                        workspace,
212                        exclude,
213                        mut crates,
214                        crates_no_std,
215                    } = doc_targets;
216
217                    let base_cmd = |no_deps: bool, document_private_items: bool| -> Vec<String> {
218                        let mut v = Vec::new();
219                        v.push("cargo".into());
220                        if let Some(rust_toolchain) = &rust_toolchain {
221                            v.push(format!("+{rust_toolchain}"))
222                        }
223                        v.push("doc".into());
224                        v.push("--message-format=json-render-diagnostics".into());
225                        v.push("--target".into());
226                        v.push(target_triple.to_string());
227                        if locked {
228                            v.push("--locked".into());
229                        }
230                        if verbose {
231                            v.push("--verbose".into());
232                        }
233                        if no_deps {
234                            v.push("--no-deps".into());
235                        }
236                        if document_private_items {
237                            v.push("--document-private-items".into())
238                        }
239                        v
240                    };
241
242                    // first command to run should be the workspace-level
243                    // command (if one was provided)
244                    if let Some((no_deps, document_private_items)) = workspace {
245                        // subsume crates with the same options
246                        crates.remove(&(no_deps, document_private_items));
247
248                        let mut v = base_cmd(no_deps, document_private_items);
249
250                        v.push("--workspace".into());
251
252                        for crates_no_std in crates_no_std.values() {
253                            for c in crates_no_std.iter().chain(exclude.iter()) {
254                                v.push("--exclude".into());
255                                v.push(c.into())
256                            }
257                        }
258
259                        cmds.push(v);
260                    }
261
262                    // subsequently: document any specific std crates
263                    for ((no_deps, document_private_items), crates) in crates {
264                        let mut v = base_cmd(no_deps, document_private_items);
265
266                        for c in crates {
267                            v.push("-p".into());
268                            v.push(c);
269                        }
270
271                        cmds.push(v)
272                    }
273
274                    // lastly: document any no_std crates
275                    for ((no_deps, document_private_items), crates) in crates_no_std {
276                        let mut v = base_cmd(no_deps, document_private_items);
277
278                        for c in crates {
279                            v.push("-p".into());
280                            v.push(c);
281                        }
282
283                        cmds.push(v)
284                    }
285
286                    let cmd = CargoDocCommands {
287                        cmds,
288                        cargo_work_dir: in_folder.clone(),
289                        no_incremental,
290                    };
291
292                    rt.write(write_doc_cmd, &cmd);
293                }
294            });
295        }
296
297        Ok(())
298    }
299}