1use git::{fetch_latest_patch_release, fetch_release, ReleaseInfo};
2use lock::{acquire_lock, wait_for_lock};
3use log::{debug, info, warn};
4use metadata::fetch_latest_release_tag;
5use std::{
6 env,
7 fs::{self, File},
8 path::{Path, PathBuf},
9};
10use util::{copy_to_dir, delete_all_except};
11use walkdir::WalkDir;
12
13use lib_version::get_lib_obs_version;
14
15use download::download_binaries;
16use zip::ZipArchive;
17
18pub use metadata::get_meta_info;
19
20mod download;
21mod git;
22mod lib_version;
23mod lock;
24mod metadata;
25mod util;
26
27fn is_ci_environment() -> bool {
29 env::var("CI").is_ok()
30 || env::var("GITHUB_ACTIONS").is_ok()
31 || env::var("GITLAB_CI").is_ok()
32 || env::var("CIRCLECI").is_ok()
33 || env::var("TRAVIS").is_ok()
34 || env::var("JENKINS_URL").is_ok()
35 || env::var("BUILDKITE").is_ok()
36}
37
38fn check_ci_environment(cache_dir: &Path) {
40 if !is_ci_environment() {
41 return;
42 }
43
44 let mut warnings = Vec::new();
45
46 if env::var("GITHUB_TOKEN").is_err() {
48 warnings.push(
49 "GITHUB_TOKEN environment variable not set in CI. \
50This may cause GitHub API rate limiting issues.",
51 );
52 }
53
54 if !cache_dir.exists() {
56 warnings.push(
57 "OBS build cache directory does not exist. \
58Consider caching this directory in your CI configuration to speed up builds. \
59Ignore if this is the first run.",
60 );
61 }
62
63 if !warnings.is_empty() {
64 println!("cargo:warning=");
65 println!("cargo:warning=⚠️ CI Environment Configuration Issues Detected:");
66 for warning in warnings {
67 println!("cargo:warning= - {}", warning);
68 }
69 println!("cargo:warning=");
70 println!("cargo:warning=For detailed setup instructions, see:");
71 println!("cargo:warning=https://github.com/libobs-rs/libobs-rs/blob/main/cargo-obs-build/CI_SETUP.md");
72 println!("cargo:warning=");
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct ObsBuildConfig {
79 pub out_dir: PathBuf,
81
82 pub cache_dir: Option<PathBuf>,
84
85 pub repo_id: Option<String>,
87
88 pub override_zip: Option<PathBuf>,
91
92 pub rebuild: bool,
94
95 pub browser: bool,
97
98 pub tag: Option<String>,
102
103 pub skip_compatibility_check: bool,
105
106 pub remove_pdbs: bool,
108}
109
110impl Default for ObsBuildConfig {
111 fn default() -> Self {
112 Self {
113 out_dir: PathBuf::from("obs-out"),
114 cache_dir: None,
115 repo_id: None,
116 override_zip: None,
117 rebuild: false,
118 browser: false,
119 tag: None,
120 skip_compatibility_check: false,
121 remove_pdbs: false,
122 }
123 }
124}
125
126pub fn install() -> anyhow::Result<()> {
143 use std::env;
144
145 let out_dir = env::var("OUT_DIR")
146 .map_err(|_| anyhow::anyhow!("OUT_DIR environment variable not set. This function should only be called from a build script."))?;
147
148 let target_dir = PathBuf::from(&out_dir);
149 let target_dir = target_dir
150 .parent()
151 .and_then(|p| p.parent())
152 .and_then(|p| p.parent())
153 .ok_or_else(|| anyhow::anyhow!("Failed to determine target directory from OUT_DIR"))?;
154
155 let config = ObsBuildConfig {
156 out_dir: target_dir.to_path_buf(),
157 ..Default::default()
158 };
159
160 build_obs_binaries(config)
161}
162
163pub fn build_obs_binaries(config: ObsBuildConfig) -> anyhow::Result<()> {
172 if cfg!(target_os = "linux") {
175 return Err(anyhow::anyhow!("Building OBS Studio from source is required on Linux. You can install binaries by running `cargo-obs-build install` separately before building your project."));
177 }
178
179 let ObsBuildConfig {
180 mut cache_dir,
181 repo_id,
182 out_dir,
183 rebuild,
184 browser,
185 mut tag,
186 override_zip,
187 skip_compatibility_check,
188 remove_pdbs,
189 } = config;
190
191 metadata::get_meta_info(&mut cache_dir, &mut tag)?;
193 let cache_dir = cache_dir.unwrap_or_else(|| PathBuf::from("obs-build"));
194
195 let mut obs_ver = None;
196 let repo_id = repo_id.unwrap_or_else(|| "obsproject/obs-studio".to_string());
197 if tag.is_none() {
198 obs_ver = Some(get_lib_obs_version()?);
199 let (major, minor, patch) = obs_ver.as_ref().unwrap();
200 let lib_tag = format!("{}.{}.{}", major, minor, patch);
201
202 match fetch_latest_patch_release(&repo_id, *major, *minor, &cache_dir) {
205 Ok(Some(found_tag)) => {
206 let parts: Vec<&str> = found_tag.trim_start_matches('v').split('.').collect();
207 let found_patch = parts
208 .get(2)
209 .and_then(|s| s.parse::<u32>().ok())
210 .unwrap_or(0);
211 if found_patch > *patch {
212 info!(
213 "Found newer libobs binaries release {} (crate: {}). Using {}",
214 found_tag, lib_tag, found_tag
215 );
216 tag = Some(found_tag);
217 } else {
218 tag = Some(lib_tag);
220 }
221 }
222 Ok(None) => {
223 tag = Some(lib_tag);
225 }
226 Err(e) => {
227 warn!("Failed to check for newer compatible libobs release: {}", e);
229 tag = Some(lib_tag);
230 }
231 }
232 }
233
234 let tag = tag.unwrap();
235 let target_out_dir = PathBuf::new().join(&out_dir);
236
237 check_ci_environment(&cache_dir);
239
240 let tag = if tag.trim() == "latest" {
241 fetch_latest_release_tag(&repo_id, &cache_dir)?
242 } else {
243 tag
244 };
245
246 if !skip_compatibility_check {
247 let (major, minor, patch) = if let Some(v) = obs_ver {
248 v
249 } else {
250 get_lib_obs_version()?
251 };
252
253 info!(
254 "Detected libobs crate version: {}.{}.{}",
255 major, minor, patch
256 );
257 let tag_parts: Vec<&str> = tag.trim_start_matches('v').split('.').collect();
258 let tag_parts = tag_parts
259 .iter()
260 .map(|e| e.parse::<u32>().unwrap_or(0))
261 .collect::<Vec<u32>>();
262
263 if tag_parts.len() < 3 {
264 info!("Warning: Could not determine libobs compatibility, tag does not have 3 parts");
265 } else {
266 let (tag_major, tag_minor, tag_patch) = (tag_parts[0], tag_parts[1], tag_parts[2]);
267 if major != tag_major || minor != tag_minor {
268 use log::warn;
269
270 warn!(
271 "libobs (crate) version {}.{}.{} may not be compatible with libobs (binaries) {}.{}.{}",
272 major, minor, patch, tag_major, tag_minor, tag_patch
273 );
274 warn!(
275 "Set the `libobs-version` in `[workspace.metadata]` to {}.{}.{} to avoid runtime issues",
276 major, minor, patch
277 );
278 } else {
279 info!(
280 "libobs (crate) version {}.{}.{} should be compatible with libobs (binaries) {}.{}.{}",
281 major, minor, patch, tag_major, tag_minor, tag_patch
282 );
283 }
284 }
285 }
286
287 let repo_dir = cache_dir.join(&tag);
288 let repo_exists = repo_dir.is_dir();
289
290 if !repo_exists {
291 fs::create_dir_all(&repo_dir)?;
292 }
293
294 let build_out = repo_dir.join("build_out");
295 let lock_file = cache_dir.join(format!("{}.lock", tag));
296 let success_file = repo_dir.join(".success");
297
298 wait_for_lock(&lock_file)?;
299
300 if !success_file.is_file() || rebuild {
301 let lock = acquire_lock(&lock_file)?;
302 if repo_exists || rebuild {
303 debug!("Cleaning up old build...");
304 delete_all_except(&repo_dir, None)?;
305 }
306
307 debug!("Fetching {} version of OBS Studio...", tag);
308
309 let release = fetch_release(&repo_id, &Some(tag.clone()), &cache_dir)?;
310 build_obs(release, &build_out, browser, remove_pdbs, override_zip)?;
311
312 File::create(&success_file)?;
313 drop(lock);
314 }
315
316 info!(
317 "Copying files from {} to {}",
318 build_out.display(),
319 target_out_dir.display()
320 );
321 copy_to_dir(&build_out, &target_out_dir, None)?;
322
323 info!("Done!");
324
325 Ok(())
326}
327
328fn build_obs(
329 release: ReleaseInfo,
330 build_out: &Path,
331 include_browser: bool,
332 remove_pdbs: bool,
333 override_zip: Option<PathBuf>,
334) -> anyhow::Result<()> {
335 fs::create_dir_all(build_out)?;
336
337 let obs_path = if let Some(e) = override_zip {
338 e
339 } else {
340 download_binaries(build_out, &release)?
341 };
342
343 let obs_archive = File::open(&obs_path)?;
344 let mut archive = ZipArchive::new(&obs_archive)?;
345
346 info!("Extracting OBS Studio binaries...");
347 archive.extract(build_out)?;
348 let bin_path = build_out.join("bin").join("64bit");
349 copy_to_dir(&bin_path, build_out, None)?;
350 fs::remove_dir_all(build_out.join("bin"))?;
351
352 clean_up_files(build_out, remove_pdbs, include_browser)?;
353
354 fs::remove_file(&obs_path)?;
355
356 Ok(())
357}
358
359fn clean_up_files(
360 build_out: &Path,
361 remove_pdbs: bool,
362 include_browser: bool,
363) -> anyhow::Result<()> {
364 let mut to_exclude = vec![
365 "obs64",
366 "frontend",
367 "obs-webrtc",
368 "obs-websocket",
369 "decklink",
370 "obs-scripting",
371 "qt6",
372 "qminimal",
373 "qwindows",
374 "imageformats",
375 "obs-studio",
376 "aja-output-ui",
377 "obs-vst",
378 ];
379
380 if remove_pdbs {
381 to_exclude.push(".pdb");
382 }
383
384 if !include_browser {
385 to_exclude.append(&mut vec![
386 "obs-browser",
387 "obs-browser-page",
388 "chrome_",
389 "resources",
390 "cef",
391 "snapshot",
392 "locales",
393 ]);
394 }
395
396 info!("Cleaning up unnecessary files...");
397 for entry in WalkDir::new(build_out).into_iter().flatten() {
398 let path = entry.path();
399 if to_exclude.iter().any(|e| {
400 path.file_name().is_some_and(|x| {
401 let x_l = x.to_string_lossy().to_lowercase();
402 x_l.contains(e) || x_l == *e
403 })
404 }) {
405 debug!("Deleting: {}", path.display());
406 if path.is_dir() {
407 fs::remove_dir_all(path)?;
408 } else {
409 fs::remove_file(path)?;
410 }
411 }
412 }
413
414 Ok(())
415}