Skip to main content

cel_build_utils/
lib.rs

1pub mod bazel;
2//mod ar;
3
4use 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
17/// Supported target triples - must match platform definitions in build/BUILD.bazel
18const SUPPORTED_TARGETS: &[&str] = &[
19    // Linux
20    "x86_64-unknown-linux-gnu",
21    "aarch64-unknown-linux-gnu",
22    "armv7-unknown-linux-gnueabi",
23    "i686-unknown-linux-gnu",
24    //"x86_64-unknown-linux-musl",
25    //"aarch64-unknown-linux-musl",
26    //"armv7-unknown-linux-musleabihf",
27    //"i686-unknown-linux-musl",
28    // Android
29    "aarch64-linux-android",
30    "armv7-linux-androideabi",
31    "x86_64-linux-android",
32    "i686-linux-android",
33    // Apple macOS
34    "x86_64-apple-darwin",
35    "aarch64-apple-darwin",
36    "arm64e-apple-darwin",
37    // Apple iOS
38    "aarch64-apple-ios",
39    "aarch64-apple-ios-sim",
40    "x86_64-apple-ios",
41    "arm64e-apple-ios",
42    // Apple tvOS
43    "aarch64-apple-tvos",
44    "aarch64-apple-tvos-sim",
45    "x86_64-apple-tvos",
46    // Apple watchOS
47    "aarch64-apple-watchos",
48    "aarch64-apple-watchos-sim",
49    "x86_64-apple-watchos-sim",
50    "arm64_32-apple-watchos",
51    "armv7k-apple-watchos",
52    // Apple visionOS
53    "aarch64-apple-visionos",
54    "aarch64-apple-visionos-sim",
55    // Windows
56    "x86_64-pc-windows-msvc",
57    // WebAssembly
58    "wasm32-unknown-emscripten",
59];
60
61/// Check if target triple is supported for non-cross builds
62fn 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    /// Exits the process on failure. Use `try_build` to handle the error.
147    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        // Short-circuit: use pre-built artifacts if CEL_PREBUILT_DIR is set.
159        // This skips the entire Bazel build, useful for environments where
160        // Bazel cannot download dependencies (e.g. corporate networks).
161        //
162        // Expected layout:
163        //   $CEL_PREBUILT_DIR/lib/libcel.a
164        //   $CEL_PREBUILT_DIR/include/  (cel-cpp + abseil + protobuf headers)
165        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        // Create install directory
214        fs::create_dir_all(&install_dir).context(format!(
215            "failed to create install_dir: {}",
216            install_dir.display()
217        ))?;
218
219        // Create include directory
220        fs::create_dir(&install_include_dir).context(format!(
221            "failed to create install_include_dir: {}",
222            install_include_dir.display()
223        ))?;
224        // Create library directory
225        fs::create_dir(&install_library_dir).context(format!(
226            "failed to create install_library_dir: {}",
227            install_library_dir.display()
228        ))?;
229
230        // Copy include files
231        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    /// Use pre-built CEL artifacts instead of building from source.
282    ///
283    /// Copies the pre-built `lib/` and `include/` trees into `$OUT_DIR/cel/install/`
284    /// so the rest of the build (cxx bridge compilation) finds them in the
285    /// same location as a normal Bazel build.
286    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        // Copy into $OUT_DIR/cel/install/ so cxx_build finds the headers
303        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        //println!("running {:?}", command);
333        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    //println!("copying dir {src:?} -> {dst:?}");
371    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        // Skip git metadata as it's been known to cause issues (#26) and
382        // otherwise shouldn't be required
383        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            //fs::create_dir_all(&dst).map_err(|e| e.to_string())?;
401            cp_r(&f.path(), &dst)?;
402        } else {
403            //println!("copying file {path:?} -> {dst:?}");
404            let _ = fs::remove_file(&dst);
405            if let Err(e) = fs::copy(&path, &dst) {
406                //println!("failed to copy {path:?} -> {dst:?}");
407                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}