gosuto_webrtc_sys_build/
lib.rs1use std::path::PathBuf;
16use std::{
17 collections::HashSet,
18 env,
19 fs::{self, File},
20 io::{self, BufRead, Write},
21 path,
22 process::Command,
23};
24
25use anyhow::{anyhow, Context, Result};
26use fs2::FileExt;
27use regex::Regex;
28use reqwest::StatusCode;
29
30pub const SCRATH_PATH: &str = "livekit_webrtc";
31pub const WEBRTC_TAG: &str = "libwebrtc-m137";
32pub const IGNORE_DEFINES: [&str; 2] = ["CR_CLANG_REVISION", "CR_XCODE_VERSION"];
33
34pub fn target_os() -> String {
35 let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
36 let target = env::var("TARGET").unwrap();
37 let is_simulator = target.ends_with("-sim");
38
39 match target_os.as_str() {
40 "windows" => "win",
41 "macos" => "mac",
42 "ios" => {
43 if is_simulator {
44 "ios-simulator"
45 } else {
46 "ios-device"
47 }
48 }
49 _ => &target_os,
50 }
51 .to_string()
52}
53
54pub fn target_arch() -> String {
55 let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
56 match target_arch.as_str() {
57 "aarch64" => "arm64",
58 "x86_64" => "x64",
59 _ => &target_arch,
60 }
61 .to_owned()
62}
63
64pub fn webrtc_triple() -> String {
67 let profile = if use_debug() { "debug" } else { "release" };
68 format!("{}-{}-{}", target_os(), target_arch(), profile)
69}
70
71pub fn use_debug() -> bool {
75 let var = env::var("LK_DEBUG_WEBRTC");
76 var.is_ok() && var.unwrap() == "true"
77}
78
79pub fn custom_dir() -> Option<path::PathBuf> {
81 if let Ok(path) = env::var("LK_CUSTOM_WEBRTC") {
82 return Some(path::PathBuf::from(path));
83 }
84 None
85}
86
87pub fn prebuilt_dir() -> path::PathBuf {
92 let target_dir = base_dir();
93 target_dir.join(format!(
94 "livekit/{}-{}/{}",
95 webrtc_triple(),
96 WEBRTC_TAG,
97 webrtc_triple()
98 ))
99}
100
101fn base_dir() -> path::PathBuf {
106 if cfg!(target_os = "windows") {
107 env::temp_dir().join(SCRATH_PATH)
108 } else {
109 scratch::path(SCRATH_PATH)
110 }
111}
112
113pub fn download_url() -> String {
114 format!(
115 "https://github.com/MaikBuse/gosuto-livekit-sdks/releases/download/{}/{}.zip",
116 WEBRTC_TAG,
117 format!("webrtc-{}", webrtc_triple())
118 )
119}
120
121pub fn webrtc_dir() -> path::PathBuf {
123 if let Some(path) = custom_dir() {
124 return path;
125 }
126
127 prebuilt_dir()
128}
129
130pub fn webrtc_defines() -> Vec<(String, Option<String>)> {
131 let defines_re = Regex::new(r"-D(\w+)(?:=([^\s]+))?").unwrap();
133 let mut files = vec![webrtc_dir().join("webrtc.ninja")];
134 if env::var("CARGO_CFG_TARGET_OS").unwrap() != "android" {
137 files.push(webrtc_dir().join("desktop_capture.ninja"));
138 }
139
140 let mut seen = HashSet::new();
141 let mut vec = Vec::new();
142
143 for path in files {
144 let gni = fs::File::open(&path)
145 .unwrap_or_else(|e| panic!("Could not open ninja file: {path:?}\n{e:?}"));
146
147 let mut defines_line = String::default();
148 io::BufReader::new(gni).read_line(&mut defines_line).unwrap();
149 for cap in defines_re.captures_iter(&defines_line) {
150 let define_name = &cap[1];
151 let define_value = cap.get(2).map(|m| m.as_str());
152 if IGNORE_DEFINES.contains(&define_name) {
153 continue;
154 }
155 let value = define_value.map(str::to_string);
156 let name = define_name.to_owned();
157 if seen.insert((name.clone(), value.clone())) {
158 vec.push((name, value));
159 }
160 }
161 }
162
163 vec
164}
165
166pub fn configure_jni_symbols() -> Result<()> {
167 download_webrtc().context("Failed to download WebRTC binaries for JNI configuration")?;
168
169 let toolchain = android_ndk_toolchain().context("Failed to locate Android NDK toolchain")?;
170 let toolchain_bin = toolchain.join("bin");
171
172 let webrtc_dir = webrtc_dir();
173 let webrtc_lib = webrtc_dir.join("lib");
174
175 let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap());
176
177 let readelf_output = Command::new(toolchain_bin.join("llvm-readelf"))
179 .arg("-Ws")
180 .arg(webrtc_lib.join("libwebrtc.a"))
181 .output()
182 .expect("failed to run llvm-readelf");
183
184 let jni_regex = Regex::new(r"(Java_org_webrtc.*)").unwrap();
185 let content = String::from_utf8_lossy(&readelf_output.stdout);
186 let jni_symbols: Vec<&str> =
187 jni_regex.captures_iter(&content).map(|cap| cap.get(1).unwrap().as_str()).collect();
188
189 if jni_symbols.is_empty() {
190 return Err(anyhow!("No JNI symbols found")); }
192
193 for symbol in &jni_symbols {
195 println!("cargo:rustc-link-arg=-Wl,--undefined={}", symbol);
196 }
197
198 let vs_path = out_dir.join("webrtc_jni.map");
200 let mut vs_file = fs::File::create(&vs_path).context("Failed to create version script file")?;
201
202 let jni_symbols = jni_symbols.join("; ");
203 write!(vs_file, "JNI_WEBRTC {{\n\tglobal: {}; \n}};", jni_symbols)
204 .context("Failed to write version script")?;
205
206 println!("cargo:rustc-link-arg=-Wl,--version-script={}", vs_path.display());
207
208 Ok(())
209}
210
211pub fn download_webrtc() -> Result<()> {
212 let dir = base_dir();
213 fs::create_dir_all(&dir).context("Failed to create base_dir")?;
214 let flock = File::create(dir.join(".lock"))
215 .context("Failed to create lock file for WebRTC download")?;
216 flock.lock_exclusive().context("Failed to acquire exclusive lock for WebRTC download")?;
217
218 let webrtc_dir = webrtc_dir();
219 if webrtc_dir.exists() {
220 return Ok(());
221 }
222
223 let mut resp = reqwest::blocking::get(download_url())
224 .context("Failed to send HTTP request to download WebRTC")?;
225 if resp.status() != StatusCode::OK {
226 return Err(anyhow!("failed to download webrtc: {}", resp.status()));
227 }
228
229 let out_dir = env::var("OUT_DIR").unwrap();
230 let tmp_path = PathBuf::from(out_dir).join("webrtc.zip");
231 let mut file = fs::File::options()
232 .write(true)
233 .read(true)
234 .create(true)
235 .open(&tmp_path)
236 .context("Failed to create temporary file for WebRTC download")?;
237 resp.copy_to(&mut file).context("Failed to write WebRTC download to temporary file")?;
238
239 let mut archive = zip::ZipArchive::new(file).context("Failed to open WebRTC zip archive")?;
240 let extract_dir = webrtc_dir.parent().unwrap();
241 fs::create_dir_all(extract_dir).context("Failed to create extraction directory")?;
242 archive.extract(extract_dir).context("Failed to extract WebRTC archive")?;
243 drop(archive);
244
245 fs::remove_file(&tmp_path).context("Failed to remove temporary WebRTC zip file")?;
246 Ok(())
247}
248
249pub fn android_ndk_toolchain() -> Result<path::PathBuf> {
250 let host_os = host_os();
251
252 let home = env::var("HOME");
253 let local = env::var("LOCALAPPDATA");
254
255 let home = if host_os == Some("linux") {
256 path::PathBuf::from(home.unwrap())
257 } else if host_os == Some("darwin") {
258 path::PathBuf::from(home.unwrap()).join("Library")
259 } else if host_os == Some("windows") {
260 path::PathBuf::from(local.unwrap())
261 } else {
262 return Err(anyhow!("Unsupported host OS"));
263 };
264
265 let ndk_dir = || -> Option<path::PathBuf> {
266 let ndk_env = env::var("ANDROID_NDK_HOME");
267 if let Ok(ndk_env) = ndk_env {
268 return Some(path::PathBuf::from(ndk_env));
269 }
270
271 let ndk_dir = home.join("Android/sdk/ndk");
272 if !ndk_dir.exists() {
273 return None;
274 }
275
276 let versions = fs::read_dir(ndk_dir.clone());
278 if versions.is_err() {
279 return None;
280 }
281
282 let version = versions
283 .unwrap()
284 .filter_map(Result::ok)
285 .filter_map(|dir| dir.file_name().to_str().map(ToOwned::to_owned))
286 .filter_map(|dir| semver::Version::parse(&dir).ok())
287 .max_by(semver::Version::cmp);
288
289 version.as_ref()?;
290
291 let version = version.unwrap();
292 Some(ndk_dir.join(version.to_string()))
293 }();
294
295 if let Some(ndk_dir) = ndk_dir {
296 let llvm_dir = if host_os == Some("linux") {
297 "linux-x86_64"
298 } else if host_os == Some("darwin") {
299 "darwin-x86_64"
300 } else if host_os == Some("windows") {
301 "windows-x86_64"
302 } else {
303 return Err(anyhow!("Unsupported host OS"));
304 };
305
306 Ok(ndk_dir.join(format!("toolchains/llvm/prebuilt/{}", llvm_dir)))
307 } else {
308 Err(anyhow!("Android NDK not found, please set ANDROID_NDK_HOME to your NDK path"))
309 }
310}
311
312fn host_os() -> Option<&'static str> {
313 let host = env::var("HOST").unwrap();
314 if host.contains("darwin") {
315 Some("darwin")
316 } else if host.contains("linux") {
317 Some("linux")
318 } else if host.contains("windows") {
319 Some("windows")
320 } else {
321 None
322 }
323}