1use std::fmt;
11use std::io::{Read, Write};
12use std::path::Path;
13use std::sync::Arc;
14
15use anyhow::Context;
16use cargo_gazelle::BazelBuildFile;
17use cargo_gazelle::args::Args;
18use cargo_gazelle::config::{CrateConfig, GlobalConfig};
19use cargo_gazelle::context::CrateContext;
20use cargo_gazelle::header::BazelHeader;
21use cargo_gazelle::targets::{
22 CargoBuildScript, ExtractCargoLints, RustBinary, RustLibrary, RustTarget, RustTest,
23};
24use cargo_toml::Manifest;
25use clap::Parser;
26use guppy::graph::{BuildTargetId, PackageMetadata};
27use md5::{Digest, Md5};
28use tracing_subscriber::EnvFilter;
29
30fn main() -> Result<(), anyhow::Error> {
31 tracing_subscriber::fmt()
32 .with_env_filter(EnvFilter::from_default_env())
33 .init();
34
35 let args = Args::try_parse()?;
36 tracing::debug!(?args, "Running with Args");
37 let path = args.path;
38
39 if args.formatter.is_none() {
40 tracing::warn!("Skipping formatting of BUILD.bazel files");
41 }
42 if args.check {
43 tracing::warn!("Running in 'check' mode, won't generate any updates.");
44 }
45
46 let mut command = guppy::MetadataCommand::new();
47 command.manifest_path(&path);
48
49 if let Some(cargo_binary) = &args.cargo {
52 command.cargo_path(cargo_binary);
53 }
54
55 let graph = command.build_graph().context("building crate graph")?;
56 let manifest = Manifest::from_path(&path).context("reading manifest")?;
57
58 let packages: Box<dyn Iterator<Item = PackageMetadata>> = match manifest.package {
60 Some(package) => {
61 let package_metadata = graph.workspace().member_by_name(package.name)?;
62 Box::new(std::iter::once(package_metadata))
63 }
64 None => Box::new(graph.workspace().iter()),
65 };
66
67 let config = Arc::new(GlobalConfig::default());
68
69 std::thread::scope(|s| {
70 let mut handles = Vec::new();
71
72 for package in packages {
74 let config = Arc::clone(&config);
75 let formatter = args.formatter.clone();
76
77 let handle = s.spawn(move || {
78 let Some(bazel_build) = generage_build_bazel(&config, &package)? else {
79 return Ok::<_, anyhow::Error>(None);
80 };
81 let bazel_build_str = bazel_build.to_string();
82
83 let crate_path = package.manifest_path().parent().ok_or_else(|| {
88 anyhow::anyhow!("Should have at least a Cargo.toml component")
89 })?;
90 let build_path = crate_path.join("BUILD.bazel").into_std_path_buf();
91
92 let mut temp_file = tempfile::NamedTempFile::new().context("creating tempfile")?;
94 tracing::debug!(?temp_file, "Writing BUILD.bazel file");
95 temp_file
96 .write_all(bazel_build_str.as_bytes())
97 .context("writing temp file")?;
98 temp_file.flush().context("flushing temp file")?;
99
100 if let Some(formatter_exec) = &formatter {
102 let result = std::process::Command::new(formatter_exec)
103 .args(["-type", "build"])
104 .arg(temp_file.path())
105 .output()
106 .context("executing formatter")?;
107 if !result.status.success() {
108 let msg = String::from_utf8_lossy(&result.stderr[..]);
109 anyhow::bail!("failed to format {build_path:?}, err: {msg}");
110 }
111 } else {
112 tracing::debug!(?crate_path, "skipping formatting");
113 }
114
115 let temp_file_hash = hash_file(temp_file.path()).context("hash temp file")?;
116 let existing_hash = hash_file(&build_path).context("hashing existing file")?;
117
118 if temp_file_hash == existing_hash {
120 tracing::debug!(?build_path, "didn't change, skipping");
121 let _ = temp_file.close();
122 Ok(None)
123 } else {
124 Ok(Some((temp_file, build_path)))
125 }
126 });
127 handles.push(handle);
128 }
129
130 let results: Vec<_> = handles
132 .into_iter()
133 .map(|handle| handle.join().expect("failed to join!"))
134 .collect::<Result<_, anyhow::Error>>()
135 .context("genrating a BUILD.bazel file")?;
136
137 let updates: Vec<_> = results.into_iter().filter_map(|x| x).collect();
138
139 if args.check && !updates.is_empty() {
140 let mut changes = Vec::new();
142 for (temp_file, dst_path) in updates {
143 let _ = temp_file.close();
144 changes.push(dst_path);
145 }
146 Err(anyhow::anyhow!(
147 "Generated files would have changed:\n{changes:?}"
148 ))
149 } else {
150 for (temp_file, dst_path) in updates {
152 let (_file, temp_path) = temp_file.keep().context("keeping temp file")?;
156 std::fs::copy(temp_path, dst_path).context("copying over temp file")?;
157 }
158 Ok::<_, anyhow::Error>(())
159 }
160 })?;
161
162 Ok(())
163}
164
165fn generage_build_bazel<'a>(
167 config: &'a GlobalConfig,
168 package: &'a PackageMetadata<'a>,
169) -> Result<Option<BazelBuildFile>, anyhow::Error> {
170 let crate_config = CrateConfig::new(package);
171 tracing::debug!(?crate_config, "found config");
172 if crate_config.skip_generating() {
173 let msg = format!(
174 "skipping generation of '{}' because `skip_generating = True` was set",
175 package.manifest_path()
176 );
177 log_info(msg);
178 return Ok(None);
179 }
180
181 let additive_content = crate_config.additive_content();
182
183 tracing::info!(path = ?package.manifest_path(), "generating");
184
185 let error_context = format!("generating {}", package.name());
186 let crate_context =
187 CrateContext::generate(config, &crate_config, package).context(error_context)?;
188
189 let build_script = CargoBuildScript::generate(config, &crate_context, &crate_config, package)?;
190 let library = RustLibrary::generate(config, package, &crate_config, build_script.as_ref())?;
191 let integration_tests: Vec<_> = package
192 .build_targets()
193 .filter(|target| matches!(target.id(), BuildTargetId::Test(_)))
194 .map(|target| RustTest::integration(config, package, &crate_config, &target))
195 .collect::<Result<_, _>>()?;
196 let binaries: Vec<_> = package
197 .build_targets()
198 .filter(|target| matches!(target.id(), BuildTargetId::Binary(_)))
199 .map(|target| RustBinary::generate(config, package, &crate_config, &target))
200 .collect::<Result<_, _>>()?;
201 let lints = ExtractCargoLints::generate();
202
203 #[allow(clippy::as_conversions)]
204 let targets: Vec<Box<dyn RustTarget>> = [Box::new(library) as Box<dyn RustTarget>]
205 .into_iter()
206 .chain(
207 build_script
208 .into_iter()
209 .map(|t| Box::new(t) as Box<dyn RustTarget>),
210 )
211 .chain(
212 integration_tests
213 .into_iter()
214 .map(|t| Box::new(t) as Box<dyn RustTarget>),
215 )
216 .chain(
217 binaries
218 .into_iter()
219 .map(|t| Box::new(t) as Box<dyn RustTarget>),
220 )
221 .chain(additive_content.map(|t| Box::new(t) as Box<dyn RustTarget>))
222 .chain(std::iter::once(lints).map(|t| Box::new(t) as Box<dyn RustTarget>))
223 .collect();
224
225 Ok(Some(BazelBuildFile {
226 header: BazelHeader::generate(&targets[..]),
227 targets,
228 }))
229}
230
231fn hash_file(file: &Path) -> Result<Option<Vec<u8>>, anyhow::Error> {
233 if !file.exists() {
234 return Ok(None);
235 }
236
237 let file = std::fs::File::open(file).context("openning file")?;
238 let mut reader = std::io::BufReader::new(file);
239 let mut contents = Vec::new();
240 reader.read_to_end(&mut contents).context("reading file")?;
241
242 let mut file_hasher = Md5::new();
243 file_hasher.update(&contents[..]);
244 let file_hash = file_hasher.finalize();
245
246 Ok(Some(file_hash.to_vec()))
247}
248
249fn log_info(s: impl fmt::Display) {
251 static YELLOW_START: &str = "\x1b[94m";
252 static RESET: &str = "\x1b[0m";
253 eprintln!("{YELLOW_START}info:{RESET} {s}");
254}