sync_readme_fixer/
main.rs

1use std::collections::BTreeMap;
2use std::io::Read;
3use std::process::{Command, Stdio};
4
5use anyhow::{Context, bail};
6use camino::{Utf8Path, Utf8PathBuf};
7use clap::Parser;
8use sync_readme_common::SyncReadmeRenderOutput;
9
10/// Executes `bazel info` to get a map of context information.
11fn bazel_info(
12    bazel: &Utf8Path,
13    workspace: Option<&Utf8Path>,
14    output_base: Option<&Utf8Path>,
15    bazel_startup_options: &[String],
16    bazel_args: &[String],
17) -> anyhow::Result<BTreeMap<String, String>> {
18    let output = bazel_command(bazel, workspace, output_base)
19        .args(bazel_startup_options)
20        .arg("info")
21        .args(bazel_args)
22        .output()?;
23
24    if !output.status.success() {
25        let status = output.status;
26        let stderr = String::from_utf8_lossy(&output.stderr);
27        bail!("bazel info failed: ({status:?})\n{stderr}");
28    }
29
30    // Extract and parse the output.
31    let info_map = String::from_utf8(output.stdout)?
32        .trim()
33        .split('\n')
34        .filter_map(|line| line.split_once(':'))
35        .map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
36        .collect();
37
38    Ok(info_map)
39}
40
41fn bazel_command(bazel: &Utf8Path, workspace: Option<&Utf8Path>, output_base: Option<&Utf8Path>) -> Command {
42    let mut cmd = Command::new(bazel);
43
44    cmd
45        // Switch to the workspace directory if one was provided.
46        .current_dir(workspace.unwrap_or(Utf8Path::new(".")))
47        .env_remove("BAZELISK_SKIP_WRAPPER")
48        .env_remove("BUILD_WORKING_DIRECTORY")
49        .env_remove("BUILD_WORKSPACE_DIRECTORY")
50        // Set the output_base if one was provided.
51        .args(output_base.map(|s| format!("--output_base={s}")));
52
53    cmd
54}
55
56// TODO(david): This shells out to an expected rule in the workspace root //:rust_analyzer that the user must define.
57// It would be more convenient if it could automatically discover all the rust code in the workspace if this target
58// does not exist.
59fn main() -> anyhow::Result<()> {
60    env_logger::init();
61
62    let config = Config::parse()?;
63
64    log::info!("running build query");
65
66    let mut command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
67        .arg("query")
68        .arg(format!(r#"kind("sync_readme rule", set({}))"#, config.targets.join(" ")))
69        .stderr(Stdio::inherit())
70        .stdout(Stdio::piped())
71        .spawn()
72        .context("bazel query")?;
73
74    let mut stdout = command.stdout.take().unwrap();
75    let mut targets = String::new();
76    stdout.read_to_string(&mut targets).context("stdout read")?;
77    if !command.wait().context("query wait")?.success() {
78        bail!("failed to run bazel query")
79    }
80
81    let items: Vec<_> = targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
82
83    let mut command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
84        .arg("cquery")
85        .args(&config.bazel_args)
86        .arg(format!("set({})", items.join(" ")))
87        .arg("--output=starlark")
88        .arg("--starlark:expr=[file.path for file in target.files.to_list()]")
89        .arg("--build")
90        .arg("--output_groups=sync_readme")
91        .stderr(Stdio::inherit())
92        .stdout(Stdio::piped())
93        .spawn()
94        .context("bazel cquery")?;
95
96    let mut stdout = command.stdout.take().unwrap();
97
98    let mut targets = String::new();
99    stdout.read_to_string(&mut targets).context("stdout read")?;
100
101    if !command.wait().context("cquery wait")?.success() {
102        bail!("failed to run bazel cquery")
103    }
104
105    let mut sync_readme_files = Vec::new();
106
107    for line in targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
108        sync_readme_files.extend(serde_json::from_str::<Vec<String>>(line).context("parse line")?);
109    }
110
111    for file in sync_readme_files {
112        let path = config.execution_root.join(&file);
113        if !path.exists() {
114            log::warn!("missing {file}");
115            continue;
116        }
117
118        let render_output = std::fs::read_to_string(path).context("read")?;
119        let render_output = serde_json::from_str::<SyncReadmeRenderOutput>(&render_output).context("render output parse")?;
120        if !render_output.path.exists() {
121            anyhow::bail!("cannot find file: {}", render_output.path);
122        }
123
124        if render_output.rendered != render_output.source {
125            log::info!("Updating {}", render_output.path);
126            std::fs::write(render_output.path, render_output.rendered).context("write output")?;
127        } else {
128            log::info!("{} already up-to-date", render_output.path);
129        }
130    }
131
132    Ok(())
133}
134
135#[derive(Debug)]
136struct Config {
137    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
138    workspace: Utf8PathBuf,
139
140    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
141    execution_root: Utf8PathBuf,
142
143    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
144    output_base: Utf8PathBuf,
145
146    /// The path to a Bazel binary.
147    bazel: Utf8PathBuf,
148
149    /// Arguments to pass to `bazel` invocations.
150    /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
151    /// for more details.
152    bazel_args: Vec<String>,
153
154    /// Space separated list of target patterns that comes after all other args.
155    targets: Vec<String>,
156}
157
158impl Config {
159    // Parse the configuration flags and supplement with bazel info as needed.
160    fn parse() -> anyhow::Result<Self> {
161        let ConfigParser {
162            workspace,
163            execution_root,
164            output_base,
165            bazel,
166            config,
167            targets,
168        } = ConfigParser::parse();
169
170        let bazel_args = vec![format!("--config={config}")];
171
172        match (workspace, execution_root, output_base) {
173            (Some(workspace), Some(execution_root), Some(output_base)) => Ok(Config {
174                workspace,
175                execution_root,
176                output_base,
177                bazel,
178                bazel_args,
179                targets,
180            }),
181            (workspace, _, output_base) => {
182                let mut info_map = bazel_info(&bazel, workspace.as_deref(), output_base.as_deref(), &[], &bazel_args)?;
183
184                let config = Config {
185                    workspace: info_map
186                        .remove("workspace")
187                        .expect("'workspace' must exist in bazel info")
188                        .into(),
189                    execution_root: info_map
190                        .remove("execution_root")
191                        .expect("'execution_root' must exist in bazel info")
192                        .into(),
193                    output_base: info_map
194                        .remove("output_base")
195                        .expect("'output_base' must exist in bazel info")
196                        .into(),
197                    bazel,
198                    bazel_args,
199                    targets,
200                };
201
202                Ok(config)
203            }
204        }
205    }
206}
207
208#[derive(Debug, Parser)]
209struct ConfigParser {
210    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
211    #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
212    workspace: Option<Utf8PathBuf>,
213
214    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
215    #[clap(long)]
216    execution_root: Option<Utf8PathBuf>,
217
218    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
219    #[clap(long, env = "OUTPUT_BASE")]
220    output_base: Option<Utf8PathBuf>,
221
222    /// The path to a Bazel binary.
223    #[clap(long, default_value = "bazel")]
224    bazel: Utf8PathBuf,
225
226    /// A config to pass to Bazel invocations with `--config=<config>`.
227    #[clap(long, default_value = "wrapper")]
228    config: String,
229
230    /// Space separated list of target patterns that comes after all other args.
231    #[clap(default_value = "@//...")]
232    targets: Vec<String>,
233}