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