use crate::error::Result;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tracing::debug;
use tracing::info;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Platform {
Linux(LinuxInfo),
MacOS(MacOSInfo),
Unsupported(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinuxInfo {
pub distro: String,
pub version: String,
pub has_mergerfs: bool,
pub mergerfs_version: Option<String>,
pub fuse_available: bool,
pub has_fusermount: bool,
pub mergerfs_path: Option<PathBuf>,
pub fusermount_path: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MacOSInfo {
pub version: String,
pub has_fuse_t: bool,
pub fuse_t_version: Option<String>,
pub has_macfuse: bool,
pub macfuse_version: Option<String>,
pub has_unionfs: bool,
pub unionfs_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct PlatformInfo {
pub platform: Platform,
#[cfg(test)]
pub arch: String,
}
impl Platform {
pub fn can_mount(&self) -> bool {
match self {
Self::Linux(info) => info.has_mergerfs && info.fuse_available,
Self::MacOS(info) => info.has_fuse_t || info.has_macfuse,
Self::Unsupported(_) => false,
}
}
pub fn mount_tool_name(&self) -> Option<&'static str> {
match self {
Self::Linux(_) => Some("mergerfs"),
Self::MacOS(_) => Some("FUSE-T or macFUSE"),
Self::Unsupported(_) => None,
}
}
}
pub fn detect_platform() -> Result<PlatformInfo> {
debug!("Starting platform detection");
#[cfg(target_os = "linux")]
{
Ok(detect_linux())
}
#[cfg(target_os = "macos")]
{
Ok(detect_macos())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let os = std::env::consts::OS;
Ok(PlatformInfo {
platform: Platform::Unsupported(os.to_string()),
#[cfg(test)]
arch: std::env::consts::ARCH.to_string(),
})
}
}
#[cfg(target_os = "linux")]
fn detect_linux() -> PlatformInfo {
let (distro, version) = detect_linux_distro();
info!("Detected Linux distribution: {} {}", distro, version);
let (mergerfs_path, mergerfs_version) = check_mergerfs();
let has_mergerfs = mergerfs_path.is_some();
if let Some(path) = &mergerfs_path {
info!(
"Found mergerfs at {} version: {}",
path.display(),
mergerfs_version.as_deref().unwrap_or("unknown")
);
} else {
info!("mergerfs not found");
}
let fuse_available = check_fuse_support();
if fuse_available {
info!("FUSE support detected");
} else {
info!("FUSE support not detected");
}
let fusermount_path = which::which("fusermount")
.or_else(|_| which::which("fusermount3"))
.ok();
let has_fusermount = fusermount_path.is_some();
if let Some(path) = &fusermount_path {
info!("fusermount detected at {}", path.display());
}
let linux_info = LinuxInfo {
distro,
version,
has_mergerfs,
mergerfs_version,
fuse_available,
has_fusermount,
mergerfs_path,
fusermount_path,
};
PlatformInfo {
platform: Platform::Linux(linux_info),
#[cfg(test)]
arch: std::env::consts::ARCH.to_string(),
}
}
#[cfg(target_os = "linux")]
fn detect_linux_distro() -> (String, String) {
if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
let mut name = "Unknown".to_string();
let mut version = "Unknown".to_string();
for line in content.lines() {
if let Some(value) = line.strip_prefix("NAME=") {
name = value.trim_matches('"').to_string();
} else if let Some(value) = line.strip_prefix("VERSION=") {
version = value.trim_matches('"').to_string();
} else if let Some(value) = line.strip_prefix("VERSION_ID=")
&& version == "Unknown"
{
version = value.trim_matches('"').to_string();
}
}
return (name, version);
}
if let Ok(output) = Command::new("lsb_release").args(["-d", "-r"]).output() {
let output_str = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = output_str.lines().collect();
let distro = lines
.first()
.and_then(|l| l.split(':').nth(1))
.map_or_else(|| "Unknown".to_string(), |s| s.trim().to_string());
let version = lines
.get(1)
.and_then(|l| l.split(':').nth(1))
.map_or_else(|| "Unknown".to_string(), |s| s.trim().to_string());
return (distro, version);
}
("Unknown Linux".to_string(), "Unknown".to_string())
}
#[cfg(target_os = "linux")]
fn check_mergerfs() -> (Option<PathBuf>, Option<String>) {
match which::which("mergerfs") {
Ok(path) => {
debug!("Found mergerfs at: {:?}", path);
let version = Command::new(&path).arg("-V").output().ok().and_then(|out| {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
extract_mergerfs_version(&stdout).or_else(|| extract_mergerfs_version(&stderr))
});
(Some(path), version)
}
Err(_) => (None, None),
}
}
#[cfg(target_os = "linux")]
fn extract_mergerfs_version(text: &str) -> Option<String> {
text.split_whitespace()
.find(|s| s.chars().any(|c| c.is_ascii_digit()))
.map(|tok| tok.trim_start_matches(['v', 'V']).to_string())
}
#[cfg(target_os = "linux")]
fn check_fuse_support() -> bool {
if Path::new("/sys/module/fuse").exists() {
return true;
}
if Path::new("/dev/fuse").exists() {
return true;
}
if let Ok(output) = Command::new("modinfo").arg("fuse").output() {
return output.status.success();
}
false
}
#[cfg(target_os = "macos")]
fn detect_macos() -> PlatformInfo {
let version = get_macos_version();
info!("Detected macOS version: {}", version);
let (has_fuse_t, fuse_t_version) = check_fuse_t();
if has_fuse_t {
info!(
"Found FUSE-T version: {}",
fuse_t_version.as_deref().unwrap_or("unknown")
);
}
let (has_macfuse, macfuse_version) = check_macfuse();
if has_macfuse {
info!(
"Found macFUSE version: {}",
macfuse_version.as_deref().unwrap_or("unknown")
);
}
use crate::platform::macos::UNIONFS_BINARIES;
let unionfs_path = UNIONFS_BINARIES
.iter()
.find_map(|binary| which::which(binary).ok());
let has_unionfs = unionfs_path.is_some();
if let Some(path) = &unionfs_path {
info!("Found unionfs at: {}", path.display());
}
let macos_info = MacOSInfo {
version,
has_fuse_t,
fuse_t_version,
has_macfuse,
macfuse_version,
has_unionfs,
unionfs_path,
};
PlatformInfo {
platform: Platform::MacOS(macos_info),
#[cfg(test)]
arch: std::env::consts::ARCH.to_string(),
}
}
#[cfg(target_os = "macos")]
fn get_macos_version() -> String {
if let Ok(output) = Command::new("sw_vers").arg("-productVersion").output() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
"Unknown".to_string()
}
}
#[cfg(target_os = "macos")]
fn check_fuse_t() -> (bool, Option<String>) {
use crate::platform::macos::FUSE_T_FS_PATH;
let fuse_t_path = Path::new(FUSE_T_FS_PATH);
if fuse_t_path.exists() {
let plist_path = fuse_t_path.join("Contents/Info.plist");
if let Ok(content) = std::fs::read_to_string(&plist_path) {
if let Some(version_start) = content.find("<key>CFBundleShortVersionString</key>") {
if let Some(version_line) = content[version_start..].lines().nth(1) {
if let Some(version) = version_line
.trim()
.strip_prefix("<string>")
.and_then(|s| s.strip_suffix("</string>"))
{
debug!("Found FUSE-T version: {}", version);
return (true, Some(version.to_string()));
}
}
}
}
debug!("Found FUSE-T but could not determine version");
return (true, None);
}
if Path::new("/usr/local/bin/go-nfsv4").exists() {
debug!("Found go-nfsv4 binary (FUSE-T component)");
return (true, None);
}
(false, None)
}
#[cfg(target_os = "macos")]
fn check_macfuse() -> (bool, Option<String>) {
let macfuse_path = Path::new("/Library/Filesystems/macfuse.fs");
if macfuse_path.exists() {
let plist_path = macfuse_path.join("Contents/Info.plist");
if let Ok(content) = std::fs::read_to_string(plist_path) {
if let Some(version_start) = content.find("<key>CFBundleShortVersionString</key>") {
if let Some(version_line) = content[version_start..].lines().nth(1) {
if let Some(version) = version_line
.trim()
.strip_prefix("<string>")
.and_then(|s| s.strip_suffix("</string>"))
{
return (true, Some(version.to_string()));
}
}
}
}
return (true, None);
}
(false, None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_detection() {
let info = detect_platform().unwrap();
match &info.platform {
Platform::Linux(_) => {
assert_eq!(std::env::consts::OS, "linux");
}
Platform::MacOS(_) => {
assert_eq!(std::env::consts::OS, "macos");
}
Platform::Unsupported(os) => {
assert_eq!(os, std::env::consts::OS);
}
}
assert!(!info.arch.is_empty());
}
#[test]
#[cfg(target_os = "linux")]
fn test_mount_tool_name_linux() {
let linux_platform = Platform::Linux(LinuxInfo {
distro: "Ubuntu".to_string(),
version: "22.04".to_string(),
has_mergerfs: true,
mergerfs_version: Some("2.33.5".to_string()),
fuse_available: true,
has_fusermount: true,
mergerfs_path: Some(PathBuf::from("/usr/bin/mergerfs")),
fusermount_path: Some(PathBuf::from("/bin/fusermount")),
});
assert_eq!(linux_platform.mount_tool_name(), Some("mergerfs"));
let unsupported = Platform::Unsupported("windows".to_string());
assert_eq!(unsupported.mount_tool_name(), None);
}
#[test]
#[cfg(target_os = "linux")]
fn test_extract_mergerfs_version() {
assert_eq!(
super::extract_mergerfs_version("mergerfs v2.40.2"),
Some("2.40.2".to_string())
);
assert_eq!(
super::extract_mergerfs_version("mergerfs V2.40.2"),
Some("2.40.2".to_string())
);
assert_eq!(
super::extract_mergerfs_version("mergerfs 2.40.2"),
Some("2.40.2".to_string())
);
assert_eq!(super::extract_mergerfs_version("no version here"), None);
assert_eq!(super::extract_mergerfs_version(""), None);
}
#[test]
#[cfg(target_os = "macos")]
fn test_mount_tool_name_macos() {
let macos_platform = Platform::MacOS(MacOSInfo {
version: "13.0".to_string(),
has_fuse_t: true,
fuse_t_version: Some("1.0.0".to_string()),
has_macfuse: false,
macfuse_version: None,
has_unionfs: true,
unionfs_path: Some(PathBuf::from("/usr/local/bin/unionfs-fuse")),
});
assert_eq!(macos_platform.mount_tool_name(), Some("FUSE-T or macFUSE"));
let unsupported = Platform::Unsupported("windows".to_string());
assert_eq!(unsupported.mount_tool_name(), None);
}
}