1use anyhow::Context as _;
2use anyhow::Result;
3use std::{
4 ffi::{OsStr, OsString},
5 path::{Path, PathBuf},
6 process::Command,
7};
8
9pub struct Bazel {
10 pub path: PathBuf,
11 pub mode: String,
12 pub wdir: Option<PathBuf>,
13 pub target: String,
14 pub options: Vec<OsString>,
15}
16
17fn mode_from_profile() -> String {
18 let profile = std::env::var("PROFILE").unwrap();
19 let mode = match profile.as_str() {
20 "debug" => "fastbuild",
21 "release" => "opt",
22 _ => "opt",
23 };
24 mode.to_owned()
25}
26
27impl Bazel {
28 pub fn new(
29 target: String,
30 minimal_version: &str,
31 maximal_version: &str,
32 download_dir: &Path,
33 download_version: Option<&str>,
34 ) -> Result<Self> {
35 let path = bazel_path(
36 minimal_version,
37 maximal_version,
38 download_dir,
39 download_version,
40 )?;
41 Ok(Self {
42 path,
43 mode: mode_from_profile(),
44 wdir: None,
45 target,
46 options: vec![],
47 })
48 }
49
50 pub fn with_work_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
51 self.wdir = Some(dir.as_ref().to_owned());
52 self
53 }
54
55 pub fn with_option(mut self, option: impl Into<OsString>) -> Self {
56 self.options.push(option.into());
57 self
58 }
59
60 pub fn with_options<I, S>(mut self, options: I) -> Self
61 where
62 I: IntoIterator<Item = S>,
63 S: AsRef<OsStr>,
64 {
65 self.options
66 .extend(options.into_iter().map(|s| s.as_ref().to_owned()));
67 self
68 }
69
70 pub fn build<I, S>(&self, targets: I) -> Command
71 where
72 I: IntoIterator<Item = S>,
73 S: AsRef<OsStr>,
74 {
75 let mut cmd = self.command("build");
76 cmd.args(targets);
77 cmd
78 }
79
80 pub fn run<I, S>(&self, targets: I) -> Command
81 where
82 I: IntoIterator<Item = S>,
83 S: AsRef<OsStr>,
84 {
85 let mut cmd = self.command("run");
86 cmd.args(targets);
87 cmd
88 }
89
90 pub fn cquery<S1: AsRef<OsStr>, S2: AsRef<str>>(&self, expr: S1, output: S2) -> Command {
91 let mut cmd = self.command("cquery");
92 cmd.arg(expr);
93 if !output.as_ref().is_empty() {
94 cmd.arg(format!("--output={}", output.as_ref()));
95 }
96 cmd
97 }
98
99 fn command<S: AsRef<OsStr>>(&self, command: S) -> Command {
100 let mut cmd = Command::new(&self.path);
101 if let Some(work_dir) = &self.wdir {
102 cmd.current_dir(work_dir);
103 }
104 cmd.args(self.options.iter().map(|s| s.as_os_str()));
105 cmd.arg(command);
106 self.common_args(&mut cmd);
107 cmd
108 }
109
110 fn common_args<'a>(&self, cmd: &'a mut Command) -> &'a mut Command {
111 cmd.arg(format!("--compilation_mode={}", self.mode));
112 cmd.arg("--verbose_failures");
113 cmd
116 }
117}
118
119fn bazel_path(
120 minimal_version: &str,
121 maximal_version: &str,
122 download_dir: &Path,
123 download_version: Option<&str>,
124) -> Result<PathBuf, anyhow::Error> {
125 let minimal_ver = semver::Version::parse(minimal_version)?;
127 let maximal_ver = semver::Version::parse(maximal_version)?;
128 if let Ok(ver) = bazel_command_version("bazel") {
129 if ver >= minimal_ver && ver <= maximal_ver {
130 return Ok(PathBuf::from("bazel"));
131 }
132 }
133 let path = download_dir.join(bazel_filename()?);
134 if let Ok(version) = bazel_command_version(&path) {
135 if version >= minimal_ver {
136 return Ok(path);
137 }
138 }
139
140 let version = download_version.unwrap_or(minimal_version);
141 download_bazel(download_dir, version)
142}
143
144fn bazel_command_version<S: AsRef<std::ffi::OsStr>>(
147 cmd: S,
148) -> Result<semver::Version, anyhow::Error> {
149 let output = std::process::Command::new(cmd)
150 .arg("--version")
151 .output()
152 .context("Failed to run bazel")?;
153 let s = String::from_utf8(output.stdout).context("Failed to parse bazel version")?;
154 let re = regex::Regex::new(r"bazel ([^\s]+)")?;
155 let caps = re
156 .captures(&s)
157 .ok_or(anyhow::anyhow!("Invalid bazel version"))?;
158 let version = caps.get(1).unwrap().as_str();
159 Ok(semver::Version::parse(version).unwrap())
160}
161
162fn bazel_url(version: &str) -> Result<(String, String), anyhow::Error> {
163 let os = match std::env::consts::OS {
164 "windows" => "windows",
165 "macos" => "darwin",
166 "linux" => "linux",
167 _ => return Err(anyhow::anyhow!("Unsupported host os")),
168 };
169 let arch = match std::env::consts::ARCH {
170 "x86_64" => "x86_64",
171 "aarch64" => "arm64",
172 _ => return Err(anyhow::anyhow!("Unsupported host arch")),
173 };
174
175 let mut url = format!("https://github.com/bazelbuild/bazel/releases/download/{version}/bazel-{version}-{os}-{arch}");
183 if os == "windows" {
184 url += ".exe";
185 }
186 let sha256_url = format!("{url}.sha256");
187 Ok((url, sha256_url))
188}
189
190fn bazel_filename() -> Result<String, anyhow::Error> {
191 let os = match std::env::consts::OS {
192 "windows" => "windows",
193 "macos" => "darwin",
194 "linux" => "linux",
195 _ => return Err(anyhow::anyhow!("Unsupported host os")),
196 };
197 if os == "windows" {
198 Ok("bazel.exe".to_string())
199 } else {
200 Ok("bazel".to_string())
201 }
202}
203
204fn parse_sha256(s: &str) -> Result<String, anyhow::Error> {
205 let mut parts = s.split_whitespace();
206 let hash = parts.next().ok_or(anyhow::anyhow!("Invalid sha256"))?;
207 let _ = parts.next().ok_or(anyhow::anyhow!("Invalid sha256"))?;
208 Ok(hash.to_string().to_lowercase())
209}
210
211fn decode_hex(s: &str) -> Result<Vec<u8>, std::num::ParseIntError> {
212 (0..s.len())
213 .step_by(2)
214 .map(|i| u8::from_str_radix(&s[i..i + 2], 16))
215 .collect()
216}
217
218fn download_bazel<P: AsRef<Path>>(dir: P, version: &str) -> Result<PathBuf, anyhow::Error> {
219 let filename_dst = bazel_filename()?;
220 let filename_tmp = format!("{filename_dst}.tmp");
221 let guard = DownloadGuard::new(
222 dir.as_ref().join(&filename_tmp),
223 dir.as_ref().join(&filename_dst),
224 )?;
225
226 let (url, sha256) = {
227 let (url, sha256_url) = bazel_url(version)?;
228 let sha256 = parse_sha256(&reqwest::blocking::get(sha256_url)?.text()?)?;
229 (url, sha256)
230 };
231
232 let dl = downloader::Download::new(&url)
233 .file_name(Path::new(&filename_tmp))
234 .verify(downloader::verify::with_digest::<sha2::Sha256>(decode_hex(
235 &sha256,
236 )?));
237
238 let mut downloader = downloader::Downloader::builder()
239 .connect_timeout(std::time::Duration::from_secs(10))
240 .download_folder(dir.as_ref())
241 .parallel_requests(3)
242 .retries(3)
243 .timeout(std::time::Duration::from_secs(60))
244 .build()?;
245 let mut results = downloader.download(&[dl])?;
246 if results.is_empty() {
247 return Err(anyhow::anyhow!("No file found"));
248 }
249 let _ = results.remove(0)?;
250
251 guard.complete()
252}
253
254struct DownloadGuard {
255 tmp: Option<PathBuf>,
256 dst: Option<PathBuf>,
257}
258
259impl DownloadGuard {
260 pub fn new(tmp: PathBuf, dst: PathBuf) -> Result<Self, anyhow::Error> {
261 if tmp.exists() {
262 std::fs::remove_file(&tmp).context("Failed to remove tmp file")?;
263 }
264 if dst.exists() {
265 std::fs::remove_file(&dst).context("Failed to remove dst file")?;
266 }
267 Ok(Self {
268 tmp: Some(tmp),
269 dst: Some(dst),
270 })
271 }
272 pub fn complete(mut self) -> Result<PathBuf, anyhow::Error> {
273 let Some(tmp) = self.tmp.take() else {
274 return Err(anyhow::anyhow!("No tmp file found"));
275 };
276 let Some(dst) = self.dst.take() else {
277 return Err(anyhow::anyhow!("No dst file found"));
278 };
279
280 #[cfg(unix)]
281 std::fs::set_permissions(&tmp, std::os::unix::fs::PermissionsExt::from_mode(0o755))
282 .context("Failed to set permissions")?;
283
284 std::fs::rename(&tmp, &dst).context("Failed to rename file")?;
285 Ok(dst)
286 }
287}
288
289impl Drop for DownloadGuard {
290 fn drop(&mut self) {
291 if let Some(tmp) = self.tmp.take() {
292 let _ = std::fs::remove_file(tmp);
293 }
294 }
295}