cel_build_utils/
bazel.rs

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.arg("--sandbox_debug");
114        //cmd.arg("--experimental_writable_outputs");
115        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    // detect bazel version
126    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
144// bazel --version output:
145// bazel 8.2.1
146fn 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    // URL examples:
176    // https://github.com/bazelbuild/bazel/releases/download/8.2.1/bazel-8.2.1-darwin-arm64
177    // https://github.com/bazelbuild/bazel/releases/download/8.2.1/bazel-8.2.1-darwin-x86_64
178    // https://github.com/bazelbuild/bazel/releases/download/8.2.1/bazel-8.2.1-linux-arm64
179    // https://github.com/bazelbuild/bazel/releases/download/8.2.1/bazel-8.2.1-linux-x86_64
180    // https://github.com/bazelbuild/bazel/releases/download/8.2.1/bazel-8.2.1-windows-arm64.exe
181    // https://github.com/bazelbuild/bazel/releases/download/8.2.1/bazel-8.2.1-windows-x86_64.exe
182    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}