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