Skip to main content

p4cli_20251/
lib.rs

1use std::io::{BufRead, Write};
2use std::process::{Command, Stdio};
3use std::sync::{Arc, Mutex};
4
5pub struct P4Cli {
6    bin_path: String,
7}
8
9#[derive(Debug)]
10pub enum P4CliResult {
11    Out(String),
12    Err(String),
13}
14
15impl std::fmt::Display for P4CliResult {
16    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
17        match self {
18            P4CliResult::Out(line) => write!(f, "{}", line),
19            P4CliResult::Err(line) => write!(f, "{}", line),
20        }
21    }
22}
23
24impl P4Cli {
25    pub fn new() -> std::io::Result<Self> {
26        let path = Self::write_p4_cli_to_disk()?;
27        Ok(Self { bin_path: path })
28    }
29
30    fn get_p4_cli_zst() -> Vec<u8> {
31        #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
32        {
33            use p4cli_20251_win_x64::get_p4_cli_zst;
34            get_p4_cli_zst()
35        }
36
37        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
38        {
39            use p4cli_20251_mac_arm64::get_p4_cli_zst;
40            get_p4_cli_zst()
41        }
42
43        #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
44        {
45            use p4cli_20251_mac_x64::get_p4_cli_zst;
46            get_p4_cli_zst()
47        }
48
49        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
50        {
51            use p4cli_20251_linux_x64::get_p4_cli_zst;
52            get_p4_cli_zst()
53        }
54
55        #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
56        {
57            use p4cli_20251_linux_arm64::get_p4_cli_zst;
58            get_p4_cli_zst()
59        }
60
61        #[cfg(not(any(
62            all(target_os = "windows", target_arch = "x86_64"),
63            all(target_os = "macos", target_arch = "aarch64"),
64            all(target_os = "macos", target_arch = "x86_64"),
65            all(target_os = "linux", target_arch = "x86_64"),
66            all(target_os = "linux", target_arch = "aarch64")
67        )))]
68        compile_error!(format!(
69            "Unsupported platform: {}-{}",
70            std::env::consts::OS,
71            std::env::consts::ARCH
72        ));
73    }
74
75    fn get_p4_cli_binary() -> std::io::Result<Vec<u8>> {
76        let zst_data = Self::get_p4_cli_zst();
77        let mut decoder = zstd::stream::Decoder::new(&zst_data[..])?;
78        let mut decompressed_data = Vec::new();
79        std::io::copy(&mut decoder, &mut decompressed_data)?;
80        Ok(decompressed_data)
81    }
82
83    fn write_p4_cli_to_disk() -> std::io::Result<String> {
84        let binary_data = Self::get_p4_cli_binary()?;
85        let temp_dir = std::env::temp_dir();
86        let file_path = temp_dir.join("p4_binary");
87
88        let mut file = std::fs::File::create(&file_path)?;
89        file.write_all(&binary_data)?;
90        file.sync_all()?;
91
92        #[cfg(unix)]
93        {
94            use std::os::unix::fs::PermissionsExt;
95            std::fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o755))?;
96        }
97
98        Ok(file_path
99            .to_str()
100            .ok_or_else(|| {
101                std::io::Error::new(std::io::ErrorKind::InvalidData, "path is not valid UTF-8")
102            })?
103            .to_string())
104    }
105
106    pub fn run<'a, I, S>(
107        &'a self,
108        args: I,
109    ) -> std::io::Result<impl Iterator<Item = std::io::Result<P4CliResult>> + 'a>
110    where
111        I: IntoIterator<Item = S>,
112        S: AsRef<std::ffi::os_str::OsStr>,
113    {
114        let process = Command::new(&self.bin_path)
115            .args(args)
116            .stdout(Stdio::piped())
117            .stderr(Stdio::piped())
118            .spawn()?;
119
120        let stdout = process
121            .stdout
122            .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
123        let stderr = process
124            .stderr
125            .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
126
127        let stdout_reader = std::io::BufReader::new(stdout).lines();
128        let stderr_reader = std::io::BufReader::new(stderr).lines();
129
130        let stdout_lines = Arc::new(Mutex::new(stdout_reader));
131        let stderr_lines = Arc::new(Mutex::new(stderr_reader));
132
133        Ok(std::iter::from_fn(move || {
134            (|| -> std::io::Result<Option<P4CliResult>> {
135                let mut stdout_lines = stdout_lines
136                    .lock()
137                    .map_err(|e| std::io::Error::other(e.to_string()))?;
138                let mut stderr_lines = stderr_lines
139                    .lock()
140                    .map_err(|e| std::io::Error::other(e.to_string()))?;
141
142                if let Some(line) = stdout_lines.next().transpose()? {
143                    return Ok(Some(P4CliResult::Out(line)));
144                }
145
146                if let Some(line) = stderr_lines.next().transpose()? {
147                    return Ok(Some(P4CliResult::Err(line)));
148                }
149
150                Ok(None)
151            })()
152            .transpose()
153        }))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_run() -> std::io::Result<()> {
163        let p4 = P4Cli::new()?;
164        let results: Vec<_> = p4
165            .run(vec!["--help".to_string()])?
166            .collect::<std::io::Result<Vec<_>>>()?;
167
168        assert!(!results.is_empty());
169
170        if let Some(P4CliResult::Out(line)) = results.first() {
171            assert!(line.contains("Usage:"));
172        } else {
173            panic!("Expected the first line to be an output with usage information.");
174        }
175
176        Ok(())
177    }
178}