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