use std::{ env, fmt, io, path::{Path, PathBuf}, }; /// Where the bundled OBS runtime's data and plugins live. /// /// Packaged builds keep OBS's own install layout and place the Clipbin /// executable where OBS expects its front end, because OBS finds helper /// executables such as `obs-ffmpeg-mux` next to the running executable: /// /// * Linux: `/bin/clipbin`, `/lib/obs-plugins`, `/share/obs`. /// * Windows: `/bin/64bit/clipbin.exe`, `/obs-plugins/64bit`, `/data`. /// /// Development builds use `CLIPBIN_OBS_RUNTIME_DIR` (see `.cargo/config.toml`), /// the directory holding `-` runtimes assembled by `scripts/obs/`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RuntimeLayout { root: PathBuf, } impl RuntimeLayout { pub fn new(root: impl Into) -> Self { Self { root: root.into() } } /// Finds the runtime for this executable, preferring the development override. pub fn discover() -> Result { // Debug builds also remember the development runtime from build time, // so binaries in target/ work when started outside Cargo. let dev_dir = env::var_os("CLIPBIN_OBS_RUNTIME_DIR") .map(PathBuf::from) .or_else(|| { cfg!(debug_assertions) .then(|| option_env!("CLIPBIN_OBS_RUNTIME_DIR").map(PathBuf::from))? }); let layout = match dev_dir { Some(dir) => Self::new(dir.join(platform_dir())), None => { let exe = env::current_exe().map_err(RuntimeError::Executable)?; Self::from_executable(&exe).ok_or_else(|| RuntimeError::Layout(exe.clone()))? } }; layout.check()?; Ok(layout) } fn from_executable(exe: &Path) -> Option { let bin_dir = exe.parent()?; let root = if cfg!(windows) { bin_dir.parent()?.parent()? } else { bin_dir.parent()? }; Some(Self::new(root)) } pub fn root(&self) -> &Path { &self.root } pub fn libobs_data_dir(&self) -> PathBuf { if cfg!(windows) { self.root.join("data").join("libobs") } else { self.root.join("share").join("obs").join("libobs") } } pub fn plugin_bin_dir(&self) -> PathBuf { if cfg!(windows) { self.root.join("obs-plugins").join("64bit") } else { self.root.join("lib").join("obs-plugins") } } pub fn plugin_data_dir(&self, plugin: &str) -> PathBuf { let base = if cfg!(windows) { self.root.join("data").join("obs-plugins") } else { self.root.join("share").join("obs").join("obs-plugins") }; base.join(plugin) } pub fn plugin_library(&self, plugin: &str) -> PathBuf { let extension = if cfg!(windows) { "dll" } else { "so" }; self.plugin_bin_dir().join(format!("{plugin}.{extension}")) } /// The OBS version recorded when the runtime was assembled. pub fn version(&self) -> Option { let manifest = std::fs::read_to_string(self.root.join("VERSION")).ok()?; manifest .lines() .find_map(|line| line.strip_prefix("obs=")) .map(str::to_owned) } fn check(&self) -> Result<(), RuntimeError> { if !self.libobs_data_dir().is_dir() { return Err(RuntimeError::Missing(self.libobs_data_dir())); } for plugin in PLUGINS { if !self.plugin_library(plugin).is_file() { return Err(RuntimeError::Missing(self.plugin_library(plugin))); } } Ok(()) } } /// The official OBS plugins Clipbin loads, and the only ones it ships. /// Keep in sync with the allowlists in scripts/obs/. #[cfg(windows)] pub const PLUGINS: &[&str] = &[ "win-capture", "win-wasapi", "obs-ffmpeg", "obs-nvenc", "obs-qsv11", ]; #[cfg(not(windows))] pub const PLUGINS: &[&str] = &[ "linux-capture", "linux-pipewire", "linux-pulseaudio", "obs-ffmpeg", "obs-nvenc", ]; /// The runtime directory name scripts/obs/ assembles for this platform, such as /// `linux-x86_64` or `windows-x86_64`. pub fn platform_dir() -> String { format!("{}-{}", env::consts::OS, env::consts::ARCH) } #[derive(Debug)] pub enum RuntimeError { Executable(io::Error), Layout(PathBuf), Missing(PathBuf), } impl fmt::Display for RuntimeError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Executable(error) => write!(formatter, "cannot locate the executable: {error}"), Self::Layout(exe) => write!( formatter, "{} is not inside an OBS runtime layout", exe.display() ), Self::Missing(dir) => write!( formatter, "the OBS runtime is incomplete: {} is missing (run scripts/obs/ to assemble it)", dir.display() ), } } } impl std::error::Error for RuntimeError {} #[cfg(test)] mod tests { use super::*; #[test] fn packaged_layout_is_relative_to_the_executable() { let (exe, root) = if cfg!(windows) { (r"C:\Clipbin\bin\64bit\clipbin.exe", r"C:\Clipbin") } else { ("/opt/clipbin/bin/clipbin", "/opt/clipbin") }; let layout = RuntimeLayout::from_executable(Path::new(exe)).unwrap(); assert_eq!(layout.root(), Path::new(root)); } #[test] fn plugin_paths_follow_the_obs_install_layout() { let layout = RuntimeLayout::new("rt"); if cfg!(windows) { assert_eq!(layout.plugin_bin_dir(), Path::new(r"rt\obs-plugins\64bit")); assert_eq!(layout.libobs_data_dir(), Path::new(r"rt\data\libobs")); } else { assert_eq!(layout.plugin_bin_dir(), Path::new("rt/lib/obs-plugins")); assert_eq!(layout.libobs_data_dir(), Path::new("rt/share/obs/libobs")); assert_eq!( layout.plugin_library("obs-ffmpeg"), Path::new("rt/lib/obs-plugins/obs-ffmpeg.so") ); assert_eq!( layout.plugin_data_dir("obs-ffmpeg"), Path::new("rt/share/obs/obs-plugins/obs-ffmpeg") ); } } }