1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use sha2::{Digest, Sha256};
8use tflite_c::TfLiteLibrary;
9
10use crate::{Error, Result};
11
12#[derive(Clone, Debug, Default)]
14pub enum Runtime {
15 #[default]
17 Auto,
18 Path(PathBuf),
20 System,
22}
23
24impl Runtime {
25 pub fn from_path(path: impl Into<PathBuf>) -> Self {
27 Self::Path(path.into())
28 }
29
30 pub(crate) fn load(&self) -> Result<Arc<TfLiteLibrary>> {
31 let explicit = match self {
32 Self::Path(path) => Some(path.clone()),
33 Self::Auto => std::env::var_os("MICRO_WAKEWORD_TFLITE_LIB")
34 .or_else(|| std::env::var_os("TFLITE_C_LIB"))
35 .map(PathBuf::from),
36 Self::System => None,
37 };
38 if let Some(path) = explicit {
39 return TfLiteLibrary::load_from_path(&path).map_err(Error::from);
40 }
41 match self {
42 Self::Auto => {
43 let path = bundled_runtime_path()?;
44 TfLiteLibrary::load_from_path(path).map_err(Error::from)
45 }
46 Self::System => TfLiteLibrary::load_default().map_err(Error::from),
47 Self::Path(_) => unreachable!(),
48 }
49 }
50
51 pub fn path(&self) -> Option<&Path> {
53 match self {
54 Self::Path(path) => Some(path),
55 Self::Auto | Self::System => None,
56 }
57 }
58}
59
60struct BundledRuntime {
61 bytes: &'static [u8],
62 sha256: &'static str,
63 version: &'static str,
64 target: &'static str,
65 file_name: &'static str,
66}
67
68#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
69fn bundled_runtime() -> BundledRuntime {
70 BundledRuntime {
71 bytes: include_bytes!("../runtime/windows-x86_64/tensorflowlite_c-2.17.1.dll"),
72 sha256: "882e6d8f9866ff84f23d4b964c145b7f0f0a8907fa830dcd8c499e7c46bf3365",
73 version: "2.17.1",
74 target: "x86_64-pc-windows",
75 file_name: "tensorflowlite_c.dll",
76 }
77}
78
79#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
80fn bundled_runtime() -> BundledRuntime {
81 BundledRuntime {
82 bytes: include_bytes!("../runtime/linux-x86_64/libtensorflowlite_c-2.17.1.so"),
83 sha256: "25465edb5cd7aadd00249d4d28f1d922ce5cc90195ad4403458111dd63493bae",
84 version: "2.17.1",
85 target: "x86_64-unknown-linux-gnu",
86 file_name: "libtensorflowlite_c.so",
87 }
88}
89
90#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
91fn bundled_runtime() -> BundledRuntime {
92 BundledRuntime {
93 bytes: include_bytes!("../runtime/linux-aarch64/libtensorflowlite_c-2.17.1.so"),
94 sha256: "12062437bfde367b1be592ebc4a9fe64b8453f90b3b7dc21fade002c38e1ac1a",
95 version: "2.17.1",
96 target: "aarch64-unknown-linux-gnu",
97 file_name: "libtensorflowlite_c.so",
98 }
99}
100
101#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
102fn bundled_runtime() -> BundledRuntime {
103 BundledRuntime {
104 bytes: include_bytes!("../runtime/macos-aarch64/libtensorflowlite_c-2.17.1.dylib"),
105 sha256: "e77597b3710e43f58f1c37a9c8979a82901ed9f3a11de71e95f2154a4c9ce6d7",
106 version: "2.17.1",
107 target: "aarch64-apple-darwin",
108 file_name: "libtensorflowlite_c.dylib",
109 }
110}
111
112#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
113fn bundled_runtime() -> BundledRuntime {
114 BundledRuntime {
115 bytes: include_bytes!("../runtime/macos-x86_64/libtensorflowlite_c-2.17.0.dylib"),
116 sha256: "6cf562771e6cb8d7856a86f859d004f0ef72861b6cafa88afbfad5d5f40261fc",
117 version: "2.17.0",
118 target: "x86_64-apple-darwin",
119 file_name: "libtensorflowlite_c.dylib",
120 }
121}
122
123#[cfg(any(
124 all(target_os = "windows", target_arch = "x86_64"),
125 all(
126 target_os = "linux",
127 any(target_arch = "x86_64", target_arch = "aarch64")
128 ),
129 all(
130 target_os = "macos",
131 any(target_arch = "x86_64", target_arch = "aarch64")
132 ),
133))]
134fn bundled_runtime_path() -> Result<PathBuf> {
135 let runtime = bundled_runtime();
136 let directory = runtime_cache_root()
137 .join("micro-wakeword")
138 .join(format!("runtime-{}-{}", runtime.version, runtime.target));
139 let destination = directory.join(runtime.file_name);
140 if has_expected_checksum(&destination, runtime.sha256)? {
141 return Ok(destination);
142 }
143
144 fs::create_dir_all(&directory).map_err(|source| Error::Io {
145 path: directory.clone(),
146 source,
147 })?;
148
149 static TEMPORARY_ID: AtomicU64 = AtomicU64::new(0);
150 let id = TEMPORARY_ID.fetch_add(1, Ordering::Relaxed);
151 let temporary = directory.join(format!(
152 ".{}.{}.{id}.tmp",
153 runtime.file_name,
154 std::process::id()
155 ));
156 let mut file = fs::OpenOptions::new()
157 .write(true)
158 .create_new(true)
159 .open(&temporary)
160 .map_err(|source| Error::Io {
161 path: temporary.clone(),
162 source,
163 })?;
164 if let Err(source) = file.write_all(runtime.bytes).and_then(|_| file.sync_all()) {
165 let _ = fs::remove_file(&temporary);
166 return Err(Error::Io {
167 path: temporary,
168 source,
169 });
170 }
171 drop(file);
172
173 if !has_expected_checksum(&temporary, runtime.sha256)? {
174 let _ = fs::remove_file(&temporary);
175 return Err(Error::InvalidConfig(
176 "embedded TensorFlow Lite runtime checksum mismatch".into(),
177 ));
178 }
179
180 if has_expected_checksum(&destination, runtime.sha256)? {
184 let _ = fs::remove_file(&temporary);
185 return Ok(destination);
186 }
187 if destination.exists() {
188 fs::remove_file(&destination).map_err(|source| Error::Io {
189 path: destination.clone(),
190 source,
191 })?;
192 }
193 if let Err(source) = fs::rename(&temporary, &destination) {
194 if has_expected_checksum(&destination, runtime.sha256)? {
195 let _ = fs::remove_file(&temporary);
196 } else {
197 let _ = fs::remove_file(&temporary);
198 return Err(Error::Io {
199 path: destination,
200 source,
201 });
202 }
203 }
204 Ok(destination)
205}
206
207#[cfg(any(
208 all(target_os = "windows", target_arch = "x86_64"),
209 all(
210 target_os = "linux",
211 any(target_arch = "x86_64", target_arch = "aarch64")
212 ),
213 all(
214 target_os = "macos",
215 any(target_arch = "x86_64", target_arch = "aarch64")
216 ),
217))]
218fn runtime_cache_root() -> PathBuf {
219 #[cfg(target_os = "windows")]
220 if let Some(path) = std::env::var_os("LOCALAPPDATA") {
221 return PathBuf::from(path);
222 }
223
224 #[cfg(target_os = "linux")]
225 if let Some(path) = std::env::var_os("XDG_CACHE_HOME") {
226 return PathBuf::from(path);
227 }
228
229 #[cfg(target_os = "macos")]
230 if let Some(path) = std::env::var_os("HOME") {
231 return PathBuf::from(path).join("Library").join("Caches");
232 }
233
234 #[cfg(target_os = "linux")]
235 if let Some(path) = std::env::var_os("HOME") {
236 return PathBuf::from(path).join(".cache");
237 }
238
239 std::env::temp_dir()
240}
241
242#[cfg(any(
243 all(target_os = "windows", target_arch = "x86_64"),
244 all(
245 target_os = "linux",
246 any(target_arch = "x86_64", target_arch = "aarch64")
247 ),
248 all(
249 target_os = "macos",
250 any(target_arch = "x86_64", target_arch = "aarch64")
251 ),
252))]
253fn has_expected_checksum(path: &Path, expected: &str) -> Result<bool> {
254 if !path.exists() {
255 return Ok(false);
256 }
257 Ok(file_sha256(path)? == expected)
258}
259
260#[cfg(any(
261 all(target_os = "windows", target_arch = "x86_64"),
262 all(
263 target_os = "linux",
264 any(target_arch = "x86_64", target_arch = "aarch64")
265 ),
266 all(
267 target_os = "macos",
268 any(target_arch = "x86_64", target_arch = "aarch64")
269 ),
270))]
271fn file_sha256(path: &Path) -> Result<String> {
272 let file = fs::File::open(path).map_err(|source| Error::Io {
273 path: path.to_owned(),
274 source,
275 })?;
276 let mut reader = std::io::BufReader::new(file);
277 let mut digest = Sha256::new();
278 let mut buffer = [0_u8; 64 * 1024];
279 loop {
280 let read = reader.read(&mut buffer).map_err(|source| Error::Io {
281 path: path.to_owned(),
282 source,
283 })?;
284 if read == 0 {
285 break;
286 }
287 digest.update(&buffer[..read]);
288 }
289 Ok(format!("{:x}", digest.finalize()))
290}
291
292#[cfg(not(any(
293 all(target_os = "windows", target_arch = "x86_64"),
294 all(
295 target_os = "linux",
296 any(target_arch = "x86_64", target_arch = "aarch64")
297 ),
298 all(
299 target_os = "macos",
300 any(target_arch = "x86_64", target_arch = "aarch64")
301 ),
302)))]
303fn bundled_runtime_path() -> Result<PathBuf> {
304 Err(Error::UnsupportedPlatform(
305 "no bundled TensorFlow Lite runtime for this target; use Runtime::Path or Runtime::System"
306 .into(),
307 ))
308}
309
310#[cfg(test)]
311mod tests {
312 #[cfg(any(
313 all(target_os = "windows", target_arch = "x86_64"),
314 all(
315 target_os = "linux",
316 any(target_arch = "x86_64", target_arch = "aarch64")
317 ),
318 all(
319 target_os = "macos",
320 any(target_arch = "x86_64", target_arch = "aarch64")
321 ),
322 ))]
323 #[test]
324 fn bundled_runtime_loads_all_required_symbols() {
325 let path = super::bundled_runtime_path().unwrap();
326 tflite_c::TfLiteLibrary::load_from_path(path).unwrap();
327 }
328}