Skip to main content

oxivgl_build/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Build-time helpers for oxivgl: PNG → LVGL image conversion.
3
4use std::path::PathBuf;
5
6/// Build-time configuration for LVGL image asset compilation.
7pub struct ImageConfig {
8    /// Path to directory containing `lv_conf.h`.
9    pub lv_conf_dir: PathBuf,
10    /// Path to LVGL header root (directory containing `lvgl.h`).
11    pub lvgl_include_dir: PathBuf,
12    /// Path to `LVGLImage.py` converter script.
13    pub converter: PathBuf,
14}
15
16impl ImageConfig {
17    /// Create config from environment.
18    ///
19    /// - `lv_conf_dir` from `DEP_LV_CONFIG_PATH` env var
20    /// - `lvgl_include_dir` and `converter` discovered from the
21    ///   `oxivgl_sys` cargo git checkout or thirdparty fallback.
22    ///
23    /// # Panics
24    /// If `DEP_LV_CONFIG_PATH` is not set or LVGL source tree not found.
25    pub fn from_env() -> Self {
26        let lv_conf_dir = PathBuf::from(
27            std::env::var("DEP_LV_CONFIG_PATH")
28                .expect("DEP_LV_CONFIG_PATH must be set (points to dir containing lv_conf.h)"),
29        );
30        let lvgl_root = find_lvgl_root();
31        ImageConfig {
32            lv_conf_dir,
33            lvgl_include_dir: lvgl_root.join("src"),
34            converter: lvgl_root.join("scripts/LVGLImage.py"),
35        }
36    }
37
38    /// Convert a PNG to an LVGL C image source, compile it, and link it.
39    ///
40    /// - `name`: C symbol name (e.g. `"cogwheel"`). Must be a valid C identifier.
41    /// - `png_path`: path to PNG file, relative to `CARGO_MANIFEST_DIR`.
42    ///
43    /// Color format is derived from `LV_COLOR_DEPTH` in `lv_conf.h`.
44    ///
45    /// # Build requirements
46    /// Python 3 with `pypng` and `lz4` packages.
47    ///
48    /// # Panics
49    /// - PNG file not found
50    /// - `LVGLImage.py` exits non-zero
51    /// - `cc` compilation fails
52    pub fn image_asset(&self, name: &str, png_path: &str) {
53        let manifest_dir =
54            PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
55        let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));
56        let png_abs = manifest_dir.join(png_path);
57        assert!(
58            png_abs.exists(),
59            "image asset not found: {}",
60            png_abs.display()
61        );
62
63        let cf = color_format_from_conf(&self.lv_conf_dir);
64
65        // Run LVGLImage.py
66        let status = std::process::Command::new("python3")
67            .arg(&self.converter)
68            .args(["--ofmt", "C"])
69            .args(["--cf", cf])
70            .args(["--align", "1"])
71            .args(["--name", name])
72            .args(["-o", out_dir.to_str().unwrap()])
73            .arg(&png_abs)
74            .status()
75            .unwrap_or_else(|e| panic!("failed to run LVGLImage.py: {e}"));
76        assert!(
77            status.success(),
78            "LVGLImage.py failed with exit code {:?}",
79            status.code()
80        );
81
82        // Compile the generated .c file
83        let c_file = out_dir.join(format!("{name}.c"));
84        assert!(
85            c_file.exists(),
86            "LVGLImage.py did not produce {}",
87            c_file.display()
88        );
89
90        cc::Build::new()
91            .file(&c_file)
92            .define("LV_LVGL_H_INCLUDE_SIMPLE", None)
93            .include(&self.lvgl_include_dir)
94            .include(&self.lv_conf_dir)
95            .opt_level(2)
96            .compile(&format!("lvgl_img_{name}"));
97
98        println!("cargo:rerun-if-changed={png_path}");
99    }
100}
101
102/// Read `LV_COLOR_DEPTH` from `lv_conf.h` and return the matching
103/// Locate the LVGL source tree.
104///
105/// Checks (in order): sibling `oxivgl-sys/lvgl`, cargo git checkouts
106/// for `oxivgl_sys-*` or `oxivgl_sys-*`, then `thirdparty/oxivgl_sys/lvgl`.
107fn find_lvgl_root() -> PathBuf {
108    // Primary: cargo metadata from oxivgl-sys (links = "lv")
109    if let Ok(dir) = std::env::var("DEP_LV_SRC_DIR") {
110        let p = PathBuf::from(dir);
111        if p.join("lv_version.h").exists() {
112            return p;
113        }
114    }
115
116    // Fallback: sibling oxivgl-sys crate (workspace layout)
117    let manifest_dir =
118        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
119    // Check both manifest_dir/oxivgl-sys/lvgl (when called from oxivgl root)
120    // and manifest_dir/../oxivgl-sys/lvgl (when called from a sibling crate)
121    for base in [
122        manifest_dir.as_path(),
123        manifest_dir.parent().unwrap_or(&manifest_dir),
124    ] {
125        let candidate = base.join("oxivgl-sys").join("lvgl");
126        if candidate.join("lv_version.h").exists() {
127            return candidate;
128        }
129    }
130
131    let cargo_home = std::env::var("CARGO_HOME")
132        .map(PathBuf::from)
133        .unwrap_or_else(|_| {
134            PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".cargo")
135        });
136    let checkouts = cargo_home.join("git/checkouts");
137    if let Ok(entries) = std::fs::read_dir(&checkouts) {
138        for entry in entries.flatten() {
139            let name = entry.file_name().to_string_lossy().to_string();
140            if name.starts_with("oxivgl_sys-") || name.starts_with("oxivgl_sys-") {
141                if let Ok(revs) = std::fs::read_dir(entry.path()) {
142                    for rev in revs.flatten() {
143                        let candidate = rev.path().join("lvgl");
144                        if candidate.join("lv_version.h").exists() {
145                            return candidate;
146                        }
147                    }
148                }
149            }
150        }
151    }
152    // Fallback: thirdparty submodule (legacy)
153    let manifest_dir =
154        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
155    let fallback = manifest_dir.join("thirdparty/oxivgl_sys/lvgl");
156    if fallback.join("lv_version.h").exists() {
157        return fallback;
158    }
159    panic!(
160        "LVGL source tree not found in oxivgl-sys/lvgl/, \
161         {}/git/checkouts/{{oxivgl_sys,oxivgl_sys}}-*/*/lvgl/, \
162         or thirdparty/oxivgl_sys/lvgl/",
163        cargo_home.display()
164    );
165}
166
167/// `LVGLImage.py` `--cf` color format string.
168fn color_format_from_conf(lv_conf_dir: &std::path::Path) -> &'static str {
169    let conf_path = lv_conf_dir.join("lv_conf.h");
170    let contents = std::fs::read_to_string(&conf_path)
171        .unwrap_or_else(|e| panic!("cannot read {}: {e}", conf_path.display()));
172
173    for line in contents.lines() {
174        let line = line.trim();
175        if line.starts_with("#define") && line.contains("LV_COLOR_DEPTH") {
176            // e.g. "#define LV_COLOR_DEPTH 16"
177            if let Some(val) = line.split_whitespace().nth(2) {
178                return match val {
179                    "16" => "RGB565",
180                    "24" => "RGB888",
181                    "32" => "ARGB8888",
182                    other => panic!(
183                        "unsupported LV_COLOR_DEPTH {other} in {} (expected 16, 24, or 32)",
184                        conf_path.display()
185                    ),
186                };
187            }
188        }
189    }
190    panic!("LV_COLOR_DEPTH not found in {}", conf_path.display());
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::io::Write;
197
198    #[test]
199    fn parse_color_depth_16() {
200        let dir = std::env::temp_dir().join("oxivgl_build_test_16");
201        std::fs::create_dir_all(&dir).unwrap();
202        let mut f = std::fs::File::create(dir.join("lv_conf.h")).unwrap();
203        writeln!(f, "#define LV_COLOR_DEPTH 16").unwrap();
204        assert_eq!(color_format_from_conf(&dir), "RGB565");
205        let _ = std::fs::remove_dir_all(&dir);
206    }
207
208    #[test]
209    fn parse_color_depth_32() {
210        let dir = std::env::temp_dir().join("oxivgl_build_test_32");
211        std::fs::create_dir_all(&dir).unwrap();
212        let mut f = std::fs::File::create(dir.join("lv_conf.h")).unwrap();
213        writeln!(f, "#define LV_COLOR_DEPTH 32").unwrap();
214        assert_eq!(color_format_from_conf(&dir), "ARGB8888");
215        let _ = std::fs::remove_dir_all(&dir);
216    }
217
218    #[test]
219    #[should_panic(expected = "unsupported LV_COLOR_DEPTH")]
220    fn parse_color_depth_unsupported() {
221        let dir = std::env::temp_dir().join("oxivgl_build_test_bad");
222        std::fs::create_dir_all(&dir).unwrap();
223        let mut f = std::fs::File::create(dir.join("lv_conf.h")).unwrap();
224        writeln!(f, "#define LV_COLOR_DEPTH 8").unwrap();
225        color_format_from_conf(&dir);
226    }
227}