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