1pub mod bazel;
2use anyhow::anyhow;
5use anyhow::Context;
6use anyhow::Result;
7use bazel::Bazel;
8use std::env;
9use std::fs;
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13const BAZEL_MINIMAL_VERSION: &str = "8.0.0";
14const BAZEL_MAXIMAL_VERSION: &str = "8.99.99";
15const BAZEL_DOWNLOAD_VERSION: Option<&str> = Some("8.3.1");
16
17const SUPPORTED_TARGETS: &[&str] = &[
19 "x86_64-unknown-linux-gnu",
21 "aarch64-unknown-linux-gnu",
22 "armv7-unknown-linux-gnueabi",
23 "i686-unknown-linux-gnu",
24 "aarch64-linux-android",
30 "armv7-linux-androideabi",
31 "x86_64-linux-android",
32 "i686-linux-android",
33 "x86_64-apple-darwin",
35 "aarch64-apple-darwin",
36 "arm64e-apple-darwin",
37 "aarch64-apple-ios",
39 "aarch64-apple-ios-sim",
40 "x86_64-apple-ios",
41 "arm64e-apple-ios",
42 "aarch64-apple-tvos",
44 "aarch64-apple-tvos-sim",
45 "x86_64-apple-tvos",
46 "aarch64-apple-watchos",
48 "aarch64-apple-watchos-sim",
49 "x86_64-apple-watchos-sim",
50 "arm64_32-apple-watchos",
51 "armv7k-apple-watchos",
52 "aarch64-apple-visionos",
54 "aarch64-apple-visionos-sim",
55 "x86_64-pc-windows-msvc",
57 "wasm32-unknown-emscripten",
59];
60
61fn is_supported_target(target: &str) -> bool {
63 SUPPORTED_TARGETS.contains(&target)
64}
65
66fn is_cross_rs() -> bool {
67 env::var("CROSS_SYSROOT").is_ok() && env::var("CROSS_TOOLCHAIN_PREFIX").is_ok()
68}
69
70fn is_windows(target: &str) -> bool {
71 target.contains("windows")
72}
73
74fn target_config(target: &str) -> Option<&'static str> {
75 if target.contains("apple") {
76 if target.contains("darwin") {
77 return Some("macos");
78 } else if target.contains("ios") {
79 return Some("ios");
80 }
81 }
82 if target.contains("windows") {
83 return Some("msvc");
84 }
85 None
86}
87
88fn work_dir(target: &str) -> Result<PathBuf> {
89 if !is_supported_target(target) {
90 return Err(anyhow::anyhow!(
91 "Unsupported target for cel-build-utils: {}. See SUPPORTED_TARGETS in cel-build-utils/src/lib.rs",
92 target
93 ));
94 }
95
96 let dir = if is_windows(target) {
97 "cel-windows"
98 } else {
99 "cel"
100 };
101
102 Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join(dir))
103}
104
105pub fn version() -> &'static str {
106 env!("CARGO_PKG_VERSION")
107}
108
109pub struct Build {
110 out_dir: Option<PathBuf>,
111 target: Option<String>,
112}
113
114impl Default for Build {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120pub struct Artifacts {
121 include_dir: PathBuf,
122 lib_dir: PathBuf,
123 libs: Vec<String>,
124 #[allow(dead_code)]
125 target: String,
126}
127
128impl Build {
129 pub fn new() -> Build {
130 Build {
131 out_dir: env::var_os("OUT_DIR").map(|s| PathBuf::from(s).join("cel")),
132 target: env::var("TARGET").ok(),
133 }
134 }
135
136 pub fn out_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Build {
137 self.out_dir = Some(path.as_ref().to_path_buf());
138 self
139 }
140
141 pub fn target(&mut self, target: &str) -> &mut Build {
142 self.target = Some(target.to_string());
143 self
144 }
145
146 pub fn build(&mut self) -> Artifacts {
148 match self.try_build() {
149 Ok(a) => a,
150 Err(e) => {
151 println!("cargo:warning=libcel: failed to build cel-cpp from source\n{e}");
152 std::process::exit(1)
153 }
154 }
155 }
156
157 pub fn try_build(&mut self) -> Result<Artifacts> {
158 if let Ok(prebuilt) = env::var("CEL_PREBUILT_DIR") {
166 return self.use_prebuilt(&prebuilt);
167 }
168
169 let target = self.target.as_ref().context("TARGET dir not set")?;
170 let out_dir = self.out_dir.as_ref().context("OUT_DIR not set")?;
171 if !out_dir.exists() {
172 fs::create_dir_all(out_dir)
173 .context(format!("failed_to create out_dir: {}", out_dir.display()))?;
174 }
175 let work_dir = work_dir(target)?;
176 let install_dir = out_dir.join("install");
177
178 let install_library_dir = install_dir.join("lib");
179 let install_include_dir = install_dir.join("include");
180 let libs = vec!["cel".to_owned()];
181
182 let mut bazel = Bazel::new(
183 target.clone(),
184 BAZEL_MINIMAL_VERSION,
185 BAZEL_MAXIMAL_VERSION,
186 out_dir,
187 BAZEL_DOWNLOAD_VERSION,
188 )?
189 .with_work_dir(&work_dir);
190
191 if is_cross_rs() {
192 bazel = bazel.with_option("--output_user_root=/tmp/bazel");
193 }
194
195 let mut build_command = bazel.build(["//:cel"]);
196
197 build_command.arg(format!("--platforms=//:{target}"));
198
199 if let Some(config) = target_config(target) {
200 build_command.arg(format!("--config={config}"));
201 }
202
203 self.run_command(build_command, "building cel")
204 .map_err(|e| anyhow!(e))?;
205
206 if install_dir.exists() {
207 fs::remove_dir_all(&install_dir).context(format!(
208 "failed to remove install_dir: {}",
209 install_dir.display()
210 ))?;
211 }
212
213 fs::create_dir_all(&install_dir).context(format!(
215 "failed to create install_dir: {}",
216 install_dir.display()
217 ))?;
218
219 fs::create_dir(&install_include_dir).context(format!(
221 "failed to create install_include_dir: {}",
222 install_include_dir.display()
223 ))?;
224 fs::create_dir(&install_library_dir).context(format!(
226 "failed to create install_library_dir: {}",
227 install_library_dir.display()
228 ))?;
229
230 let include_mapping = vec![
232 ("bazel-cel/external/cel-cpp+", "."),
233 ("bazel-cel/external/abseil-cpp+/absl", "absl"),
234 ("bazel-cel/external/protobuf+/src/google", "google"),
235 (
236 "bazel-bin/external/cel-spec+/proto/cel/expr/_virtual_includes/checked_proto/cel",
237 "cel",
238 ),
239 (
240 "bazel-bin/external/cel-spec+/proto/cel/expr/_virtual_includes/value_proto/cel",
241 "cel",
242 ),
243 (
244 "bazel-bin/external/cel-spec+/proto/cel/expr/_virtual_includes/syntax_proto/cel",
245 "cel",
246 ),
247 ];
248
249 for (f, t) in include_mapping {
250 #[cfg(windows)]
251 let f = f.replace("bazel-cel", "bazel-cel-windows");
252 #[cfg(windows)]
253 let f = f.replace("/", "\\");
254 #[cfg(windows)]
255 let t = t.replace("/", "\\");
256
257 let f = work_dir.join(f);
258 let t = install_include_dir.join(t);
259 cp_r(&f, &t)?;
260 }
261
262 let libcel_name = if is_windows(target) {
263 "cel.lib"
264 } else {
265 "libcel.a"
266 };
267 std::fs::copy(
268 work_dir.join("bazel-bin").join(libcel_name),
269 install_library_dir.join(libcel_name),
270 )
271 .context(format!("failed to copy {libcel_name}"))?;
272
273 Ok(Artifacts {
274 lib_dir: install_library_dir,
275 include_dir: install_include_dir,
276 libs,
277 target: target.to_owned(),
278 })
279 }
280
281 fn use_prebuilt(&self, prebuilt_dir: &str) -> Result<Artifacts> {
287 let dir = PathBuf::from(prebuilt_dir);
288 let src_lib = dir.join("lib");
289 let src_include = dir.join("include");
290
291 let libcel = if cfg!(windows) { "cel.lib" } else { "libcel.a" };
292 anyhow::ensure!(
293 src_lib.join(libcel).exists(),
294 "CEL_PREBUILT_DIR={prebuilt_dir}: {libcel} not found in {}",
295 src_lib.display()
296 );
297 anyhow::ensure!(
298 src_include.exists(),
299 "CEL_PREBUILT_DIR={prebuilt_dir}: include/ directory not found"
300 );
301
302 let out_dir = self.out_dir.as_ref().context("OUT_DIR not set")?;
304 let install_dir = out_dir.join("install");
305 if install_dir.exists() {
306 fs::remove_dir_all(&install_dir)?;
307 }
308 let install_lib = install_dir.join("lib");
309 let install_inc = install_dir.join("include");
310 fs::create_dir_all(&install_lib)?;
311 cp_r(&src_include, &install_inc)?;
312 fs::copy(src_lib.join(libcel), install_lib.join(libcel))?;
313
314 println!(
315 "cargo:warning=Using pre-built CEL from {} (skip Bazel)",
316 dir.display()
317 );
318
319 Ok(Artifacts {
320 lib_dir: install_lib,
321 include_dir: install_inc,
322 libs: vec!["cel".to_owned()],
323 target: self
324 .target
325 .clone()
326 .unwrap_or_else(|| "unknown".to_owned()),
327 })
328 }
329
330 #[track_caller]
331 fn run_command(&self, mut command: Command, desc: &str) -> Result<Vec<u8>, String> {
332 let output = command.output();
334
335 let verbose_error = match output {
336 Ok(output) => {
337 let status = output.status;
338 if status.success() {
339 return Ok(output.stdout);
340 }
341 let stdout = String::from_utf8_lossy(&output.stdout);
342 let stderr = String::from_utf8_lossy(&output.stderr);
343 format!(
344 "'{exe}' reported failure with {status}\nstdout: {stdout}\nstderr: {stderr}",
345 exe = command.get_program().to_string_lossy()
346 )
347 }
348 Err(failed) => match failed.kind() {
349 std::io::ErrorKind::NotFound => format!(
350 "Command '{exe}' not found. Is {exe} installed?",
351 exe = command.get_program().to_string_lossy()
352 ),
353 _ => format!(
354 "Could not run '{exe}', because {failed}",
355 exe = command.get_program().to_string_lossy()
356 ),
357 },
358 };
359
360 println!("cargo:warning={desc}: {verbose_error}");
361 Err(format!(
362 "Error {desc}:
363 {verbose_error}
364 Command failed: {command:?}"
365 ))
366 }
367}
368
369fn cp_r(src: &Path, dst: &Path) -> Result<()> {
370 for f in fs::read_dir(src).map_err(|e| anyhow!("{}: {e}", src.display()))? {
372 let f = match f {
373 Ok(f) => f,
374 _ => continue,
375 };
376 fs::create_dir_all(dst).map_err(|e| anyhow!("failed to create dir {dst:?}: {e}"))?;
377
378 let file_name = f.file_name();
379 let mut path = f.path();
380
381 if file_name.to_str() == Some(".git") {
384 continue;
385 }
386
387 let dst = dst.join(file_name);
388 let mut ty = f.file_type().map_err(|e| anyhow!("failed to read file type {f:?}: {e}"))?;
389 while ty.is_symlink() {
390 let link_path = fs::read_link(f.path()).map_err(|e| anyhow!("failed to read link {f:?}: {e}"))?;
391 if link_path.is_relative() {
392 path = f.path().parent().unwrap().join(link_path);
393 } else {
394 path = link_path;
395 }
396 ty = fs::metadata(&path).map_err(|e| anyhow!("failed to read metadata {path:?}: {e}"))?.file_type();
397 }
398
399 if ty.is_dir() {
400 cp_r(&f.path(), &dst)?;
402 } else {
403 let _ = fs::remove_file(&dst);
405 if let Err(e) = fs::copy(&path, &dst) {
406 return Err(anyhow!(
408 "failed to copy '{}' to '{}': {e}",
409 path.display(),
410 dst.display()
411 ));
412 }
413 }
414 }
415 Ok(())
416}
417
418#[allow(dead_code)]
419fn sanitize_sh(path: &Path) -> String {
420 if !cfg!(windows) {
421 return path.to_string_lossy().into_owned();
422 }
423 let path = path.to_string_lossy().replace("\\", "/");
424 return change_drive(&path).unwrap_or(path);
425
426 fn change_drive(s: &str) -> Option<String> {
427 let mut ch = s.chars();
428 let drive = ch.next().unwrap_or('C');
429 if ch.next() != Some(':') {
430 return None;
431 }
432 if ch.next() != Some('/') {
433 return None;
434 }
435 Some(format!("/{}/{}", drive, &s[drive.len_utf8() + 2..]))
436 }
437}
438
439impl Artifacts {
440 pub fn include_dir(&self) -> &Path {
441 &self.include_dir
442 }
443
444 pub fn lib_dir(&self) -> &Path {
445 &self.lib_dir
446 }
447
448 pub fn libs(&self) -> &[String] {
449 &self.libs
450 }
451
452 pub fn print_cargo_metadata(&self) {
453 println!("cargo:rustc-link-search=native={}", self.lib_dir.display());
454 for lib in self.libs.iter() {
455 println!("cargo:rustc-link-lib=static={lib}");
456 }
457 println!("cargo:include={}", self.include_dir.display());
458 println!("cargo:lib={}", self.lib_dir.display());
459 }
460}