unity-cli 0.15.1

Rust CLI for Unity Editor automation over the Unity TCP protocol
use std::fs;
#[cfg(unix)]
use std::hash::{Hash, Hasher};
use std::path::PathBuf;

use anyhow::Result;

use crate::core::runtime_paths::RuntimePaths;

#[derive(Debug, Clone)]
pub struct DaemonRuntimePaths {
    name: &'static str,
    paths: RuntimePaths,
}

impl DaemonRuntimePaths {
    pub fn new(name: &'static str) -> Result<Self> {
        Ok(Self {
            name,
            paths: RuntimePaths::discover()?,
        })
    }

    pub fn dir(&self) -> Result<PathBuf> {
        self.paths.daemon_dir(self.name)
    }

    pub fn pid_file(&self) -> Result<PathBuf> {
        Ok(self.dir()?.join(format!("{}.pid", self.name)))
    }

    #[cfg(unix)]
    pub fn socket_file(&self) -> Result<PathBuf> {
        let preferred = self.dir()?.join(format!("{}.sock", self.name));
        if preferred.as_os_str().len() < 100 {
            return Ok(preferred);
        }

        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        preferred.hash(&mut hasher);
        let shortened = std::env::temp_dir().join(format!(
            "unity-cli-{}-{:016x}.sock",
            self.name,
            hasher.finish()
        ));
        Ok(shortened)
    }

    pub fn cleanup(&self) {
        if let Ok(path) = self.pid_file() {
            let _ = fs::remove_file(path);
        }
        #[cfg(unix)]
        {
            if let Ok(path) = self.socket_file() {
                let _ = fs::remove_file(path);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::process::Command;

    use tempfile::tempdir;

    #[test]
    fn runtime_module_builds_without_warnings_when_unix_code_is_disabled() {
        let dir = tempdir().expect("tempdir should succeed");

        // Probe: try compiling a trivial file to check the target is usable.
        let probe_path = dir.path().join("probe.rs");
        fs::write(&probe_path, "fn main() {}").expect("write probe");
        let probe = Command::new("rustc")
            .arg("--edition=2021")
            .arg("--target")
            .arg("x86_64-pc-windows-msvc")
            .arg("--emit=metadata")
            .arg("--out-dir")
            .arg(dir.path())
            .arg("--crate-name")
            .arg("target_probe")
            .arg(&probe_path)
            .output();
        if probe.as_ref().map_or(true, |o| !o.status.success()) {
            eprintln!("skipping: x86_64-pc-windows-msvc target not available");
            return;
        }

        let source_path = dir.path().join("runtime_windows_check.rs");
        let runtime_path = format!("{}/src/daemon/runtime.rs", env!("CARGO_MANIFEST_DIR"));
        let source = format!(
            r##"#![allow(dead_code)]

extern crate self as anyhow;
pub type Result<T> = std::result::Result<T, std::io::Error>;

mod core {{
    pub mod runtime_paths {{
        use std::path::PathBuf;

        use crate::Result;

        #[derive(Debug, Clone)]
        pub struct RuntimePaths;

        impl RuntimePaths {{
            pub fn discover() -> Result<Self> {{
                Ok(Self)
            }}

            pub fn daemon_dir(&self, name: &str) -> Result<PathBuf> {{
                Ok(std::env::temp_dir().join(name))
            }}
        }}
    }}
}}

mod daemon {{
    pub mod runtime {{
        include!(r#"{runtime_path}"#);
    }}
}}

fn main() {{
    let paths = daemon::runtime::DaemonRuntimePaths::new("unityd").expect("runtime paths");
    let _ = paths.dir();
    let _ = paths.pid_file();
    paths.cleanup();
}}
"##
        );
        fs::write(&source_path, source).expect("fixture source should be written");

        let output = Command::new("rustc")
            .arg("--edition=2021")
            .arg("--target")
            .arg("x86_64-pc-windows-msvc")
            .arg("-D")
            .arg("warnings")
            .arg("--emit=metadata")
            .arg("--out-dir")
            .arg(dir.path())
            .arg("--crate-name")
            .arg("runtime_windows_check")
            .arg(&source_path)
            .output()
            .expect("rustc should run");

        assert!(
            output.status.success(),
            "runtime.rs should compile cleanly when unix-only code is disabled\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
}