sync_readme_fixer/
main.rs1use 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
10fn 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 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 .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 .args(output_base.map(|s| format!("--output_base={s}")));
50
51 cmd
52}
53
54fn 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 workspace: Utf8PathBuf,
137
138 execution_root: Utf8PathBuf,
140
141 output_base: Utf8PathBuf,
143
144 bazel: Utf8PathBuf,
146
147 bazel_args: Vec<String>,
151
152 targets: Vec<String>,
154}
155
156impl Config {
157 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 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
210 workspace: Option<Utf8PathBuf>,
211
212 #[clap(long)]
214 execution_root: Option<Utf8PathBuf>,
215
216 #[clap(long, env = "OUTPUT_BASE")]
218 output_base: Option<Utf8PathBuf>,
219
220 #[clap(long, default_value = "bazel")]
222 bazel: Utf8PathBuf,
223
224 #[clap(long, default_value = "wrapper")]
226 config: String,
227
228 #[clap(default_value = "@//...")]
230 targets: Vec<String>,
231}