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}